Kurs NestJS · Moduł 7: Testowanie
Performance Testing - próby wytrzymałościowe
W tej lekcji5
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.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.
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: 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:
Arrangeprzygotowuje dane i atrapy,Actwywołuje badaną metodę,Assertsprawdza wynik, aCleanupsprząta, - więcej niż jedno wywołanie w sekcji
Actznaczy, że test bada dwie rzeczy naraz, - suite to blok
describe; przygotowanie nazywamy setup, sprzątanie - teardown, beforeAlliafterAllobsługują to, co kosztowne i jednorazowe;beforeEachiafterEach- to, co musi być świeże przy każdym teście,- co otworzył
beforeAll, zamykaafterAll- 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.
Kod do tej lekcji: src/performance-testing.ts
1// Performance Testing - Proby Wytrzymalosciowe Fortu
2
3// Narzedzia do testowania wydajnosci w NestJS
4
5// 1. Mierzenie czasu wykonania
6class PerformanceTester {
7 static async measureExecution<T>(
8 fn: () => Promise<T>,
9 label: string
10 ): Promise<{ result: T; duration: number }> {
11 const start = performance.now();
12 const result = await fn();
13 const duration = performance.now() - start;
14 console.log(`[PERF] ${label}: ${duration.toFixed(2)}ms`);
15 return { result, duration };
16 }
17
18 // Testuj obciazenie - symuluj wielu uzytkownikow
19 static async loadTest(
20 fn: () => Promise<any>,
21 concurrency: number,
22 iterations: number
23 ) {
24 const results: number[] = [];
25
26 for (let i = 0; i < iterations; i++) {
27 const promises = Array.from({ length: concurrency }, () => {
28 const start = performance.now();
29 return fn().then(() => performance.now() - start);
30 });
31 const times = await Promise.all(promises);
32 results.push(...times);
33 }
34
35 const sorted = [...results].sort((a, b) => a - b);
36 const avg = results.reduce((a, b) => a + b, 0) / results.length;
37
38 return {
39 totalRequests: results.length,
40 avgResponseTime: avg.toFixed(2) + 'ms',
41 maxResponseTime: Math.max(...results).toFixed(2) + 'ms',
42 minResponseTime: Math.min(...results).toFixed(2) + 'ms',
43 p95: sorted[Math.floor(results.length * 0.95)].toFixed(2) + 'ms',
44 };
45 }
46}
47
48// 2. Serwis z roznymi zlozonosci operacji
49class LegionQueryService {
50 private legions: Array<{ id: number; name: string; soldiers: number }>;
51
52 constructor() {
53 this.legions = Array.from({ length: 10000 }, (_, i) => ({
54 id: i + 1,
55 name: `Legio ${i + 1}`,
56 soldiers: Math.floor(Math.random() * 10000),
57 }));
58 }
59
60 findByName(name: string) {
61 return this.legions.find(l => l.name === name); // O(n)
62 }
63
64 findById(id: number) {
65 return this.legions[id - 1]; // O(1)
66 }
67
68 getTopLegions(count: number) {
69 return [...this.legions]
70 .sort((a, b) => b.soldiers - a.soldiers)
71 .slice(0, count); // O(n log n)
72 }
73}
74
75// 3. Benchmark
76async function runBenchmark() {
77 const service = new LegionQueryService();
78
79 console.log('=== BENCHMARK: Wyszukiwanie Legionow ===');
80
81 await PerformanceTester.measureExecution(
82 async () => service.findById(5000),
83 'findById (O(1))'
84 );
85
86 await PerformanceTester.measureExecution(
87 async () => service.findByName('Legio 5000'),
88 'findByName (O(n))'
89 );
90
91 await PerformanceTester.measureExecution(
92 async () => service.getTopLegions(10),
93 'getTopLegions (O(n log n))'
94 );
95}
96Widzisz błąd w tej lekcji?
Zadania praktyczne w grze
- Edytor kodu
Stwórz pełny suite testowy z setup, teardown i edge cases
- Układanie w poziomie
Ułóż poprawną konwencję nazewnictwa pliku testowego:
- Klikanie w kolejności
Ułóż wzorzec AAA (Arrange-Act-Assert) testu: