Kurs JavaScript i TypeScript · Moduł 11: Testowanie z Jest

Setup i Teardown

5 min czytania
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. afterAll

beforeAll 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. 1. Kiedy wykonuje się funkcja beforeEach w Jest?

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

Przydatne artykuły