Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Performance Testing - próby wytrzymałościowe

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.

Gdzie mieszka test

Zacznijmy od nazwy pliku, bo tu obowiązuje konwencja, której trzyma się cały ekosystem NestJS:

1legions.service.spec.ts

Czyta 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.

Wzorzec AAA - trzy takty jednej próby

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.

Haki - kiedy przygotować, kiedy posprzątać

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ł

beforeAll
, zamyka
afterAll
; co przygotował
beforeEach
, sprząta
afterEach
. Niedomknięte połączenie sprawi, że Jest zawiesi się po ostatnim teście, wypisując, że coś jeszcze działa.

Przypadki brzegowe (edge cases) - gdzie machiny pękają

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.

Podsumowanie

Plac prób uporządkowany, machiny zmierzone:

  • testy wydajnościowe pytają nie czy kod działa, ale jak długo i pod jakim naporem,
  • nazwa pliku to konwencja, po której Jest wyszukuje testy:
    nazwa.service.spec.ts
    , a dla E2E
    .e2e-spec.ts
    ,
  • wzorzec AAA:
    Arrange
    przygotowuje dane i atrapy,
    Act
    wywołuje badaną metodę,
    Assert
    sprawdza wynik, a
    Cleanup
    sprząta,
  • więcej niż jedno wywołanie w sekcji
    Act
    znaczy, że test bada dwie rzeczy naraz,
  • suite to blok
    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,
  • co otworzył
    beforeAll
    , zamyka
    afterAll
    - niedomknięte połączenie zawiesza Jest po ostatnim teście,
  • przypadki brzegowe dla wytrzymałości to zero, jeden i bardzo dużo; ostatni z nich to spike,
  • progi czasowe ustawiaj z zapasem - test padający losowo zostanie wyłączony i przestanie chronić cokolwiek.

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.

Przejdź do CodeWorlds