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

Dlaczego testowanie jest ważne?

5 min czytania
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:

  1. Wykrywanie błędów wcześnie - im wcześniej znajdziesz błąd, tym taniej go naprawisz
  2. Dokumentacja zachowania - testy opisują, jak kod powinien działać
  3. Bezpieczne refaktorowanie - możesz zmieniać kod wiedząc, że testy wychwycą regresje
  4. 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. 1. Która warstwa piramidy testów zawiera najwięcej testów?

Przydatne artykuły