Legion sprawdził już, czy machiny działają. Zostało pytanie inne: ile wytrzymają? Katapulta, która oddaje jeden strzał, jest bezużyteczna przy oblężeniu trwającym tydzień. Testy wydajnościowe mierzą właśnie to - nie czy kod działa, ale jak długo i pod jakim naporem.
Takie próby są jednak wyjątkowo wrażliwe na bałagan. Test, który zostawia po sobie tysiąc rekordów w bazie, zafałszuje wynik następnego. Dlatego zanim zmierzymy wytrzymałość machin, ułożymy plac prób w porządku - i ten porządek przyda Ci się w każdym teście, nie tylko wydajnościowym.
Zacznijmy od nazwy pliku, bo tu obowiązuje konwencja, której trzyma się cały ekosystem NestJS:
1legions.service.spec.tsCzyta się ją od lewej: co testujemy (
legions), jakiego rodzaju to element (service), że to plik testowy (spec), i wreszcie rozszerzenie (ts). Testy E2E dostają zamiast tego .e2e-spec.ts.Ta konwencja nie jest ozdobnikiem - Jest domyślnie wyszukuje pliki po tym wzorcu. Plik nazwany
testLegions.ts po prostu nigdy się nie uruchomi, a Ty będziesz się zastanawiać, czemu warta w CI świeci na zielono. Plik trzymamy obok testowanego kodu, dzięki czemu widać na pierwszy rzut oka, co ma testy, a co nie.Blok
describe grupujący testy jednego elementu nazywamy suite - zestawem prób. Wewnątrz niego każdy test układa się w ten sam rytm, znany jako AAA:1it('obsługuje 100 zapytań poniżej 2 sekund', async () => {
2 // Arrange - przygotuj dane i atrapy
3 const legions = Array.from({ length: 100 }, (_, i) => ({ id: i, name: `Legio ${i}` }));
4 jest.spyOn(repo, 'find').mockResolvedValue(legions);
5
6 // Act - wywołaj testowaną metodę
7 const start = Date.now();
8 await Promise.all(Array.from({ length: 100 }, () => service.findAll()));
9 const duration = Date.now() - start;
10
11 // Assert - sprawdź wynik
12 expect(duration).toBeLessThan(2000);
13});Trzy takty odpowiadają trzem słowom. Arrange przygotowuje scenę: dane, atrapy, stan początkowy. Act to jedno wywołanie - to, co właśnie badamy. Assert sprawdza wynik jedną lub kilkoma asercjami.
Wartość tego podziału jest praktyczna: gdy test padnie, od razu wiesz, w którym takcie szukać. A gdy w sekcji Act znajdziesz więcej niż jedno wywołanie, to znak, że test bada dwie rzeczy naraz i warto go rozdzielić.
Do trzech taktów dochodzi czwarty, o którym łatwo zapomnieć: Cleanup - posprzątanie po teście, w żargonie nazywane teardown. Nie zapisujemy go zwykle w ciele testu, tylko w osobnym haku, i to właśnie on decyduje o rzetelności prób wydajnościowych.
Jest daje cztery haki, a ich wybór sprowadza się do jednego pytania: czy to ma się dziać raz, czy przy każdym teście?
1describe('LegionsService - wydajność', () => {
2 let service: LegionsService;
3 let module: TestingModule;
4
5 beforeAll(async () => {
6 module = await Test.createTestingModule({
7 providers: [LegionsService, { provide: getRepositoryToken(Legion), useValue: repo }],
8 }).compile();
9
10 service = module.get(LegionsService);
11 });
12
13 afterEach(() => {
14 jest.clearAllMocks();
15 });
16
17 afterAll(async () => {
18 await module.close();
19 });
20});beforeAll uruchamia się raz przed wszystkimi testami w bloku - tu budujemy moduł testowy, bo to operacja kosztowna i nie ma powodu powtarzać jej sto razy. beforeEach działałby przed każdym testem; tam trafia to, co musi być świeże, na przykład czyszczenie tabeli.Po drugiej stronie
afterEach sprząta po każdym teście - jak tutaj zerowanie atrap - a afterAll zamyka to, co otworzył beforeAll: połączenia, moduł, pliki.Zasada jest symetryczna i warto ją zapamiętać: co otworzył
, zamyka beforeAll
; co przygotował afterAll
, sprząta beforeEach
. Niedomknięte połączenie sprawi, że Jest zawiesi się po ostatnim teście, wypisując, że coś jeszcze działa.afterEach
Test przy typowym obciążeniu mówi niewiele. Najciekawsze jest to, co dzieje się na krańcach, a dla prób wytrzymałościowych krańce są trzy: zero, jeden i bardzo dużo.
1describe('findAll pod obciążeniem', () => {
2 it('zwraca pustą tablicę, gdy legionów brak', async () => {
3 jest.spyOn(repo, 'find').mockResolvedValue([]);
4
5 await expect(service.findAll()).resolves.toEqual([]);
6 });
7
8 it('nie przekracza 5 sekund przy 10 000 legionów', async () => {
9 jest.spyOn(repo, 'find').mockResolvedValue(makeLegions(10_000));
10
11 const start = Date.now();
12 await service.findAll();
13
14 expect(Date.now() - start).toBeLessThan(5000);
15 });
16});Pierwszy test pilnuje pustki - tam, gdzie kod lubi rzucić błędem przy
legions[0]. Drugi to spike: nagły, wielokrotnie większy ładunek, sprawdzający, czy metoda skaluje się rozsądnie, czy zaczyna dławić się przy każdym rekordzie z osobna.Jedna uwaga o progach czasowych, @name: ustawiaj je z zapasem. Maszyna w CI bywa wolniejsza od Twojej, a test, który pada losowo raz na dziesięć przebiegów, zostanie wyłączony przez pierwszą osobę, której zablokuje wdrożenie - i słusznie. Lepszy próg luźny, ale wiarygodny, niż ostry i ignorowany.
Plac prób uporządkowany, machiny zmierzone:
nazwa.service.spec.ts, a dla E2E .e2e-spec.ts,Arrange przygotowuje dane i atrapy, Act wywołuje badaną metodę, Assert sprawdza wynik, a Cleanup sprząta,Act znaczy, że test bada dwie rzeczy naraz,describe; przygotowanie nazywamy setup, sprzątanie - teardown,beforeAll i afterAll obsługują to, co kosztowne i jednorazowe; beforeEach i afterEach - to, co musi być świeże przy każdym teście,beforeAll, zamyka afterAll - niedomknięte połączenie zawiesza Jest po ostatnim teście,W następnej lekcji te próby przestaną zależeć od Twojej pamięci - wpuścimy je do automatycznej warty w CI. A na razie zapamiętaj: wzorzec AAA to trzy takty jednej próby, a haki pilnują, żeby każda zaczynała się na czystym placu.