Kurs JavaScript i TypeScript · Moduł 11: Testowanie z Jest
Setup i Teardown
W tej lekcji5
Wyobraź sobie trzy testy, które korzystają z jednego wspólnego wybiegu. Pierwszy wpuszcza T-Rexa, drugi sprawdza, czy wybieg jest pusty - i pada, bo T-Rex wciąż tam siedzi. Testy, które dzielą stan, zaczynają od siebie zależeć: wynik zależy od kolejności uruchomienia, a błąd w jednym przewraca kolejne jak kostki domina.
W Parku Jurajskim przed każdą inspekcją wybiegu trzeba przygotować sprzęt, a po inspekcji posprzątać. W testach jest dokładnie tak samo - funkcje setup i teardown pozwalają przygotować i posprzątać środowisko testowe. W Jest nazywamy je hookami, a są cztery: beforeEach, afterEach, beforeAll i afterAll.
beforeEach - przed każdym testem
beforeEach wykonuje się przed każdym testem w bloku describe. To idealne miejsce na tworzenie świeżych danych testowych. Zmienne deklarujesz przez let w zasięgu describe, a wartości przypisujesz w hooku. Fabryki createEnclosure i createDinosaur znasz z poprzednich lekcji:
1describe('DinosaurEnclosure', () => {
2 let enclosure;
3 let rex;
4
5 beforeEach(() => {
6 // Przed każdym testem tworzymy nowy wybieg i dinozaura
7 enclosure = createEnclosure('Zone A', 5);
8 rex = createDinosaur('Rex', 'T-Rex', 'carnivore');
9 });
10
11 it('should add dinosaur to enclosure', () => {
12 enclosure.add(rex);
13 expect(enclosure.dinosaurs).toHaveLength(1);
14 expect(enclosure.dinosaurs[0].name).toBe('Rex');
15 });
16
17 it('should start with empty enclosure', () => {
18 // Dzięki beforeEach enclosure jest zawsze świeży!
19 expect(enclosure.dinosaurs).toHaveLength(0);
20 });
21
22 it('should track capacity', () => {
23 enclosure.add(rex);
24 expect(enclosure.remainingCapacity()).toBe(4);
25 });
26});Drugi test przechodzi, choć pierwszy dodał Rexa do wybiegu. Stary obiekt nie zniknął magicznie - po prostu beforeEach przypisał do zmiennej enclosure zupełnie nowy wybieg. To sekcja Arrange ze wzorca AAA, przeniesiona do hooka, żeby nie powtarzać jej w każdym teście.
afterEach - po każdym teście
afterEach wykonuje się po każdym teście. Służy do czyszczenia po testach - zamykania połączeń, resetowania stanu. W przykładzie SecuritySystem to klasa systemu bezpieczeństwa: onAlarm rejestruje funkcję wywoływaną przy alarmie, simulateBreach symuluje naruszenie ogrodzenia, a deactivate wyłącza system:
1describe('SecuritySystem', () => {
2 let securitySystem;
3 let alarmLog;
4
5 beforeEach(() => {
6 securitySystem = new SecuritySystem();
7 alarmLog = [];
8 securitySystem.onAlarm((msg) => alarmLog.push(msg));
9 });
10
11 afterEach(() => {
12 // Resetuj system bezpieczeństwa po każdym teście
13 securitySystem.deactivate();
14 alarmLog = [];
15 console.log('System zresetowany po teście');
16 });
17
18 it('should detect breach', () => {
19 securitySystem.simulateBreach('Zone A');
20 expect(alarmLog).toContain('Naruszenie w Zone A');
21 });
22
23 it('should handle multiple alarms', () => {
24 securitySystem.simulateBreach('Zone A');
25 securitySystem.simulateBreach('Zone B');
26 expect(alarmLog).toHaveLength(2);
27 });
28});afterEach wykona się również wtedy, gdy test nie przejdzie, dlatego to najlepsze miejsce na etap sprzątania (cleanup). Linia z console.log pokazuje tylko moment wykonania - w prawdziwych testach nie jest potrzebna.
beforeAll i afterAll - raz dla całej grupy
beforeAll uruchamia się raz przed wszystkimi testami w describe, a afterAll raz po wszystkich. Używaj ich dla kosztownych operacji. connectToDatabase to przykładowa asynchroniczna funkcja łącząca z bazą. Hooki, tak jak testy, mogą być async, a Jest poczeka na ich zakończenie:
1describe('DatabaseTests', () => {
2 let database;
3
4 beforeAll(async () => {
5 // Kosztowna operacja - połączenie z bazą, tylko raz!
6 database = await connectToDatabase();
7 console.log('Baza danych podłączona');
8 });
9
10 afterAll(async () => {
11 // Zamknij połączenie po wszystkich testach
12 await database.close();
13 console.log('Baza danych odłączona');
14 });
15
16 beforeEach(async () => {
17 // Wyczyść dane przed każdym testem
18 await database.clear();
19 });
20
21 it('should save dinosaur', async () => {
22 await database.save({ name: 'Rex', species: 'T-Rex' });
23 const dinos = await database.findAll();
24 expect(dinos).toHaveLength(1);
25 });
26
27 it('should find by species', async () => {
28 await database.save({ name: 'Rex', species: 'T-Rex' });
29 await database.save({ name: 'Blue', species: 'Velociraptor' });
30
31 const raptors = await database.findBySpecies('Velociraptor');
32 expect(raptors).toHaveLength(1);
33 expect(raptors[0].name).toBe('Blue');
34 });
35});Połączenie powstaje tylko raz, ale dane czyścimy przed każdym testem w beforeEach. To typowy duet: drogi zasób współdzielony, tani stan odświeżany. Moja rada: wszystko, co da się szybko odtworzyć, twórz w beforeEach, a beforeAll zostaw dla naprawdę kosztownych operacji. Współdzielony stan to najczęstsze źródło testów, które przechodzą tylko w określonej kolejności.
Jak rozpoznać taki przeciek? Test przechodzi, gdy uruchomisz go samodzielnie, a pada w całym zestawie. Dokumentacja Jesta radzi wtedy tymczasowo zamienić test na test.only (albo it na it.only), żeby uruchomić tylko ten jeden przypadek, i poszukać stanu, który zmienia inny test.
Kolejność wykonywania
Ważne jest zrozumienie kolejności, w jakiej Jest wykonuje setup i teardown. Poniższy blok wypisuje w konsoli numer kroku z każdego hooka i z każdego testu:
1describe('Kolejność wykonywania', () => {
2 beforeAll(() => console.log('1. beforeAll'));
3 afterAll(() => console.log('5. afterAll'));
4 beforeEach(() => console.log('2. beforeEach'));
5 afterEach(() => console.log('4. afterEach'));
6
7 it('test 1', () => console.log('3. test 1'));
8 it('test 2', () => console.log('3. test 2'));
9});
10
11// Wynik:
12// 1. beforeAll
13// 2. beforeEach
14// 3. test 1
15// 4. afterEach
16// 2. beforeEach
17// 3. test 2
18// 4. afterEach
19// 5. afterAllbeforeAll i afterAll pojawiają się w wyniku tylko raz, a para beforeEach i afterEach otacza każdy test osobno. Jest ma jeszcze jedną cechę, o której warto wiedzieć: najpierw wykonuje wszystkie funkcje describe w pliku, zbierając listę testów, a dopiero potem uruchamia same testy. Kod wpisany bezpośrednio w describe, poza hookiem, wykona się więc wcześniej, niż się spodziewasz.
Zagnieżdżone describe
Setup i teardown działają hierarchicznie w zagnieżdżonych describe. Przypomina to strefy parku, które mają własne procedury, ale obowiązuje je też regulamin całego parku:
1describe('Park Jurajski', () => {
2 beforeEach(() => console.log('Setup: Park'));
3
4 describe('Strefa Drapieżników', () => {
5 beforeEach(() => console.log('Setup: Strefa Drapieżników'));
6
7 it('T-Rex jest w wybiegu', () => {
8 console.log('Test: T-Rex');
9 // Kolejność setup: Park -> Strefa Drapieżników
10 });
11 });
12
13 describe('Strefa Roślinożerców', () => {
14 beforeEach(() => console.log('Setup: Strefa Roślinożerców'));
15
16 it('Triceratops jest w wybiegu', () => {
17 console.log('Test: Triceratops');
18 // Kolejność setup: Park -> Strefa Roślinożerców
19 });
20 });
21});Zagnieżdżone describe dziedziczą setup z nadrzędnych bloków. beforeEach z rodzica wykonuje się przed beforeEach dziecka. Przy sprzątaniu kolejność się odwraca: najpierw afterEach dziecka, potem rodzica. Hooki zadeklarowane na najwyższym poziomie pliku, poza jakimkolwiek describe, dotyczą każdego testu w tym pliku.
W lekcji o mockowaniu użyjesz beforeEach do czyszczenia sztucznych funkcji, a przy kodzie asynchronicznym do włączania sztucznych zegarów. W laboratorium poniżej zobaczysz kolejność hooków wypisaną krok po kroku.
Pamiętaj: beforeEach to poranna inspekcja wybiegu - każdy test zaczyna dzień na czystym, sprawdzonym terenie.
Kod do tej lekcji: index.js
1// Setup i Teardown - beforeEach, afterEach, beforeAll, afterAll
2console.log("=== Park Jurajski - Inspekcja Wybiegow ===\n");
3
4// Symulacja setup/teardown
5const log = [];
6let currentDescribe = "";
7
8function logStep(step) {
9 log.push(step);
10 console.log(` ${step}`);
11}
12
13// --- Demonstracja kolejnosci ---
14console.log("--- Kolejnosc wykonywania ---\n");
15
16// Symulacja
17const steps = [];
18
19function simulateTestRun() {
20 // beforeAll
21 steps.push("1. beforeAll - polaczenie z baza");
22
23 // Test 1
24 steps.push("2. beforeEach - tworzenie srodowiska");
25 steps.push("3. TEST 1 - sprawdzenie wybiegu");
26 steps.push("4. afterEach - czyszczenie");
27
28 // Test 2
29 steps.push("5. beforeEach - tworzenie srodowiska");
30 steps.push("6. TEST 2 - sprawdzenie sensora");
31 steps.push("7. afterEach - czyszczenie");
32
33 // afterAll
34 steps.push("8. afterAll - zamkniecie bazy");
35}
36
37simulateTestRun();
38steps.forEach(s => console.log(s));
39
40// --- Praktyczny przyklad ---
41console.log("\n--- Praktyczny przyklad: beforeEach ---\n");
42
43function createEnclosure(name, capacity) {
44 return {
45 name,
46 capacity,
47 dinosaurs: [],
48 add(dino) { this.dinosaurs.push(dino); },
49 remainingCapacity() { return this.capacity - this.dinosaurs.length; }
50 };
51}
52
53// Symulacja testow z beforeEach
54let enclosure;
55let rex;
56
57function beforeEach() {
58 enclosure = createEnclosure("Zone A", 5);
59 rex = { name: "Rex", species: "T-Rex" };
60 console.log(" [setup] Nowy wybieg i dinozaur stworzony");
61}
62
63// Test 1
64beforeEach();
65enclosure.add(rex);
66console.log(` [test1] Dinozaurow: ${enclosure.dinosaurs.length} -> ${enclosure.dinosaurs.length === 1 ? "PASS" : "FAIL"}`);
67
68// Test 2 - wybieg jest swiezy dzieki beforeEach!
69beforeEach();
70console.log(` [test2] Pusty wybieg: ${enclosure.dinosaurs.length} -> ${enclosure.dinosaurs.length === 0 ? "PASS" : "FAIL"}`);
71
72// Test 3
73beforeEach();
74enclosure.add(rex);
75console.log(` [test3] Remaining: ${enclosure.remainingCapacity()} -> ${enclosure.remainingCapacity() === 4 ? "PASS" : "FAIL"}`);
76
77// --- Zagniezdzone describe ---
78console.log("\n--- Zagniezdzone describe ---\n");
79
80function simulateNested() {
81 const order = [];
82
83 // Outer beforeEach
84 order.push("Park: beforeEach");
85
86 // Inner: Drapiezniki
87 order.push(" Drapiezniki: beforeEach");
88 order.push(" TEST: T-Rex w wybiegu");
89 order.push(" Drapiezniki: afterEach");
90 order.push("Park: afterEach");
91
92 order.push(""); // separator
93
94 order.push("Park: beforeEach");
95 order.push(" Roslinozerce: beforeEach");
96 order.push(" TEST: Triceratops w wybiegu");
97 order.push(" Roslinozerce: afterEach");
98 order.push("Park: afterEach");
99
100 return order;
101}
102
103simulateNested().forEach(s => console.log(s));
104
105// --- beforeAll vs beforeEach ---
106console.log("\n--- beforeAll vs beforeEach ---\n");
107console.log("beforeAll: raz przed WSZYSTKIMI testami (np. polaczenie z baza)");
108console.log("beforeEach: przed KAZDYM testem (np. czyszczenie danych)");
109console.log("afterEach: po KAZDYM tescie (np. reset stanu)");
110console.log("afterAll: raz po WSZYSTKICH testach (np. zamkniecie polaczenia)");Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Kiedy wykonuje się funkcja beforeEach w Jest?
2. Jaka jest różnica między beforeAll a beforeEach?
Zadania praktyczne w grze
- Edytor kodu
Napisz testy systemu ogrodzeń z beforeEach zapewniającym czysty stan.
- Układanie w pionie
Ułóż kolejność wykonywania funkcji setup/teardown w Jest:
- Klikanie w kolejności
Ułóż elementy deklaracji beforeEach:
- Edytor kodu
Stwórz testy z zagnieżdżonymi describe i osobnymi beforeEach.