Kurs JavaScript i TypeScript · Moduł 11: Testowanie z Jest
Dlaczego testowanie jest ważne?
W tej lekcji4
Wyobraź sobie Park Jurajski bez systemu weryfikacji DNA dinozaurów. Każdy nowo sklonowany gatunek mógłby mieć ukryte defekty genetyczne, które ujawniłyby się dopiero wtedy, gdy tyranozaur wyrwie się z wybiegu. W świecie programowania testowanie pełni dokładnie tę samą rolę - to system wczesnego ostrzegania, który wyłapuje błędy zanim trafią do użytkowników.
"Każdą próbkę sprawdzamy, zanim cokolwiek wykluje się z jaja" - powtarza Dr. Rex, szef laboratorium. W tym module zbudujesz taki system kontroli dla kodu: zestaw testów, które same pilnują, czy funkcje parku robią to, co obiecują.
Czego się nauczysz
- pisać testy w Jest i czytać raport PASS/FAIL
- dobierać matchery jak czujniki do konkretnego zagrożenia
- przygotowywać i sprzątać wybieg przed każdym testem
- podmieniać groźne zależności na bezpieczne mocki
- testować kod asynchroniczny i przewijać czas sztucznym zegarem
- pisać testy przed kodem metodą TDD i testować TypeScript
Testowanie manualne vs automatyczne
Testowanie manualne
Testowanie manualne to jak osobiste sprawdzanie każdego dinozaura w parku - czasochłonne, podatne na ludzkie błędy i niemożliwe do powtórzenia w identyczny sposób. Zobacz to na przykładzie funkcji calculateDinosaurAge, która liczy wiek okazu na podstawie roku wyklucia. Sprawdzamy ją "na oko", wypisując wyniki w konsoli:
1// Testowanie manualne - musisz sam sprawdzić wynik
2function calculateDinosaurAge(birthYear) {
3 return 2024 - birthYear;
4}
5
6// Ręczne sprawdzenie w konsoli:
7console.log(calculateDinosaurAge(2020)); // Czy to 4? Muszę sam sprawdzić...
8console.log(calculateDinosaurAge(2015)); // A to 9? Znowu sam sprawdzam...Kod działa, ale cała weryfikacja odbywa się w Twojej głowie: musisz pamiętać, że 2024 minus 2020 to 4. Nic nie zostaje zapisane, więc po każdej zmianie funkcji inspekcję trzeba powtarzać od zera.
Testowanie automatyczne
Testowanie automatyczne to jak zainstalowanie sensorów w każdym wybiegu - działają 24/7, natychmiast alertują o problemach i zawsze sprawdzają te same warunki. W kodzie funkcja test nadaje sprawdzeniu nazwę, expect przyjmuje rzeczywisty wynik, a toBe porównuje go z wartością oczekiwaną - szczegóły poznasz w następnej lekcji.
1// Testowanie automatyczne - komputer sprawdza za Ciebie
2function calculateDinosaurAge(birthYear) {
3 return 2024 - birthYear;
4}
5
6// Test automatyczny:
7test('oblicza wiek dinozaura', () => {
8 expect(calculateDinosaurAge(2020)).toBe(4);
9 expect(calculateDinosaurAge(2015)).toBe(9);
10 expect(calculateDinosaurAge(2024)).toBe(0);
11});
12// Wynik: PASS lub FAIL - natychmiast wiesz!Oczekiwania są teraz zapisane w kodzie, więc sprawdzenie trwa milisekundy i za każdym razem daje tę samą odpowiedź. Sama funkcja się nie zmieniła - zmieniło się tylko to, kto pilnuje wyniku. Uważaj jednak na pułapkę: rok 2024 jest wpisany na sztywno i w funkcji, i w oczekiwaniach, więc testy przejdą także w 2030 roku, kiedy funkcja będzie już zaniżać wiek. Test sprawdza wyłącznie to, o co go zapytasz.
Rodzaje testów
Piramida testów
W świecie testowania istnieje koncepcja piramidy testów. Na jej dole znajdują się testy jednostkowe (unit tests), w środku testy integracyjne (integration tests), a na szczycie testy end-to-end (E2E). Model spopularyzował Mike Cohn w książce Succeeding with Agile. W przykładzie poniżej getSpecies zamienia skrót na pełną nazwę gatunku, getDinosaur i feedDinosaur to dwa współpracujące moduły parku, a page to przeglądarka sterowana przez narzędzie do testów E2E - metody goto, click i fill pochodzą z Playwrighta.
1// 1. Testy jednostkowe (Unit Tests) - testują pojedynczą funkcję
2// Najszybsze, najtańsze, najwięcej ich potrzebujemy
3test('sprawdza gatunek dinozaura', () => {
4 expect(getSpecies('T-Rex')).toBe('Tyrannosaurus Rex');
5});
6
7// 2. Testy integracyjne (Integration Tests) - testują współpracę modułów
8// Sprawdzają, czy moduły poprawnie ze sobą komunikują
9test('system karmienia współpracuje z bazą dinozaurów', () => {
10 const dino = getDinosaur('Rex-001');
11 const result = feedDinosaur(dino, 'meat');
12 expect(result.status).toBe('fed');
13 expect(dino.hunger).toBe(0);
14});
15
16// 3. Testy E2E (End-to-End) - testują całą aplikację
17// Najwolniejsze, najdroższe, najmniej ich potrzebujemy
18test('użytkownik może zarezerwować wizytę w parku', async () => {
19 await page.goto('/booking');
20 await page.click('#select-date');
21 await page.fill('#name', 'John Hammond');
22 await page.click('#submit');
23 expect(await page.textContent('.confirmation')).toContain('Rezerwacja potwierdzona');
24});Wszystkie trzy testy kończą się asercją, ale różnią się zasięgiem. Test jednostkowy nie dotyka bazy ani sieci, więc trwa ułamek milisekundy i od razu wskazuje winną funkcję. Test E2E uruchamia przeglądarkę i całą aplikację - jest najbliżej prawdziwego zwiedzającego, ale gdy zawiedzie, wiesz tylko, że problem leży gdzieś na trasie od kasy do rezerwacji.
Fundament i szczyt piramidy
Pod piramidą często dorysowuje się fundament: analizę statyczną. Linter (np. ESLint) i system typów TypeScriptu sprawdzają kod, zanim uruchomisz jakikolwiek test, i wyłapią literówkę albo tekst podany zamiast liczby. Na szczycie, ponad testami E2E, zostaje odrobina testów manualnych, tak zwanych eksploracyjnych: człowiek szuka dziwnych zachowań, których nikt nie przewidział. Pełna kolejność od podstawy brzmi więc: analiza statyczna, testy jednostkowe, integracyjne, E2E i eksploracyjne. Z trzech klasycznych pięter najliczniejsze są testy jednostkowe.
Proporcje testów
Ile testów każdego rodzaju? Google Testing Blog w 2015 roku zaproponował jako dobry punkt wyjścia taki podział:
- 70% testów jednostkowych - szybkie, precyzyjne, łatwe do napisania
- 20% testów integracyjnych - sprawdzają współpracę między modułami
- 10% testów E2E - symulują prawdziwego użytkownika
To heurystyka, a nie prawo natury - autorzy sami zaznaczają, że każdy zespół dobierze własne liczby, byle zachować kształt piramidy. Odwrócona piramida, pełna powolnych testów E2E i ręcznego klikania, to znany antywzorzec nazywany rożkiem lodów (ice cream cone).
Korzyści z testowania
Testowanie to nie strata czasu - to inwestycja. Oto główne korzyści:
- Wykrywanie błędów wcześnie - im wcześniej znajdziesz błąd, tym taniej go naprawisz
- Dokumentacja zachowania - testy opisują, jak kod powinien działać
- Bezpieczne refaktorowanie - możesz zmieniać kod wiedząc, że testy wychwycą regresje
- Pewność przy wdrożeniach - zielone testy oznaczają gotowość do produkcji
Moja rada: zaczynaj od testów jednostkowych dla małych, czystych funkcji, takich jak calculateDinosaurAge. Są najtańsze w pisaniu i najszybciej odwdzięczają się złapanymi błędami. Już w następnej lekcji poznasz Jest, czyli narzędzie, które uruchamia takie testy.
W laboratorium poniżej działa miniaturowy runner testów napisany w czystym JavaScripcie. Zwróć uwagę na ostatni test z dopiskiem "bug": przechodzi, choć pilnuje ujemnego wieku. Zielony test jest tylko tak mądry, jak oczekiwanie, które w nim zapisałeś.
Pamiętaj: test to czujnik na wybiegu - raz zamontowany, pilnuje dinozaura przy każdej zmianie kodu, dzień i noc.
Kod do tej lekcji: index.js
1// Testowanie JS/TS - Dlaczego testowanie jest ważne?
2console.log("=== Park Jurajski - System Weryfikacji DNA ===\n");
3
4// Testowanie manualne vs automatyczne
5
6// PRZYKŁAD 1: Testowanie manualne (problematyczne)
7function calculateDinosaurAge(birthYear) {
8 return 2024 - birthYear;
9}
10
11console.log("--- Testowanie manualne ---");
12console.log("Wiek (2020):", calculateDinosaurAge(2020)); // Sam sprawdzam: czy to 4?
13console.log("Wiek (2015):", calculateDinosaurAge(2015)); // Sam sprawdzam: czy to 9?
14console.log("Wiek (2024):", calculateDinosaurAge(2024)); // Sam sprawdzam: czy to 0?
15
16// PRZYKŁAD 2: Testowanie automatyczne (niezawodne)
17function autoTest(description, actual, expected) {
18 const passed = actual === expected;
19 const icon = passed ? "PASS" : "FAIL";
20 console.log(`[${icon}] ${description}: got ${actual}, expected ${expected}`);
21 return passed;
22}
23
24console.log("\n--- Testowanie automatyczne ---");
25autoTest("Wiek dinozaura z 2020", calculateDinosaurAge(2020), 4);
26autoTest("Wiek dinozaura z 2015", calculateDinosaurAge(2015), 9);
27autoTest("Wiek dinozaura z 2024", calculateDinosaurAge(2024), 0);
28
29// PRZYKŁAD 3: Piramida testów
30console.log("\n--- Piramida testów ---");
31console.log("Baza: Unit Tests (70%) - szybkie, precyzyjne");
32console.log("Srodek: Integration (20%) - wspolpraca modulow");
33console.log("Szczyt: E2E Tests (10%) - cala aplikacja");
34
35// Symulacja prostego test runnera
36console.log("\n--- Mini Test Runner ---");
37
38function runTests(suiteName, tests) {
39 console.log(`\nSuite: ${suiteName}`);
40 let passed = 0;
41 let failed = 0;
42
43 tests.forEach(({ name, fn }) => {
44 try {
45 fn();
46 console.log(` [PASS] ${name}`);
47 passed++;
48 } catch (e) {
49 console.log(` [FAIL] ${name}: ${e.message}`);
50 failed++;
51 }
52 });
53
54 console.log(` Results: ${passed} passed, ${failed} failed`);
55}
56
57function assertEqual(actual, expected) {
58 if (actual !== expected) {
59 throw new Error(`Expected ${expected}, got ${actual}`);
60 }
61}
62
63runTests("DinosaurAge", [
64 {
65 name: "calculates age correctly",
66 fn: () => assertEqual(calculateDinosaurAge(2020), 4)
67 },
68 {
69 name: "handles current year",
70 fn: () => assertEqual(calculateDinosaurAge(2024), 0)
71 },
72 {
73 name: "handles negative age (bug!)",
74 fn: () => assertEqual(calculateDinosaurAge(2025), -1)
75 }
76]);Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Która warstwa piramidy testów zawiera najwięcej testów?