Kurs JavaScript i TypeScript · Moduł 11: Testowanie z Jest
Framework Jest - podstawy
W tej lekcji5
W poprzedniej lekcji test pojawił się jak gotowy czujnik. Tylko kto go uruchamia, zbiera wyniki i podnosi alarm, gdy coś pójdzie nie tak? Potrzebujesz centrum kontroli, które samo znajdzie pliki z testami, wykona je i zamelduje, co przeszło, a co nie.
Jest należy do najpopularniejszych narzędzi do testowania JavaScriptu i TypeScriptu. Powstał w Facebooku (dziś Meta), a od 2022 roku rozwija go OpenJS Foundation. W naszym Parku Jurajskim Jest będzie głównym narzędziem do weryfikacji systemów bezpieczeństwa - w tym module opieramy się na jego wersji 30.
Instalacja i konfiguracja
Jest instalujesz jako zależność deweloperską: trafia do sekcji devDependencies w pliku package.json, bo potrzebujesz go podczas pracy nad kodem, a nie na produkcji. Oto komendy, od których zaczyna się każdy projekt:
1# Instalacja Jest
2npm install --save-dev jest
3
4# Dla TypeScript
5npm install --save-dev jest ts-jest @types/jest
6
7# Inicjalizacja konfiguracji
8npm init jest@latestPierwsza komenda wystarczy do testowania JavaScriptu, druga dokłada paczki dla TypeScriptu - wrócimy do nich w lekcji o testowaniu TS. Ostatnia uruchamia kreator, który zada kilka pytań i zapisze plik konfiguracyjny. W starszych wersjach służyła do tego komenda npx jest --init, ale Jest 30 ją usunął. Warto też dopisać w package.json skrypt "test": "jest" - wtedy całą baterię testów uruchomisz przez npm test.
Struktura testu
Każdy test w Jest składa się z trzech kluczowych elementów: describe, it (lub test) oraz expect. Nie musisz ich importować, bo Jest udostępnia je jako funkcje globalne w każdym pliku testowym. Same pliki też znajdzie bez Twojej pomocy: domyślnie uruchamia wszystko, co kończy się na .test.js lub .spec.js (także w wariantach TypeScript), oraz pliki w folderze __tests__.
describe - grupowanie testów
describe to blok grupujący powiązane testy. Pomyśl o nim jak o wybiegu w parku - grupuje dinozaury tego samego gatunku. Pierwszy argument to nazwa grupy, drugi to funkcja, w której umieszczasz testy:
1describe('DinosaurEnclosure', () => {
2 // Wszystkie testy związane z wybiegiem dinozaurów
3});Sam describe niczego nie sprawdza. Porządkuje raport: w wynikach zobaczysz nazwę DinosaurEnclosure, a pod nią wszystkie testy z tego wybiegu. Bloki można zagnieżdżać, co zobaczysz za chwilę w pełnym przykładzie.
it / test - pojedynczy przypadek testowy
it i test są zamienne - oba definiują pojedynczy test. Dokumentacja Jesta opisuje it wprost jako alias test: to ta sama funkcja pod dwiema nazwami. Konwencja: it czyta się jak zdanie w języku angielskim. W jednym describe możesz nawet mieszać obie formy:
1describe('DinosaurEnclosure', () => {
2 it('should have a fence', () => {
3 // sprawdzenie
4 });
5
6 test('fence is electrified', () => {
7 // sprawdzenie
8 });
9});Oba testy działają identycznie. Pierwszy czyta się jak zdanie "it should have a fence", czyli "powinien mieć ogrodzenie". Polecam it wewnątrz describe, a test dla testów luzem, ale najważniejsza jest konsekwencja w całym projekcie.
expect - asercje
expect to serce każdego testu. Porównuje rzeczywisty wynik z oczekiwanym. Zapis zawsze ma dwie części: expect(wartość) opakowuje wynik, a matcher, np. toBe czy toEqual, mówi, czego oczekujesz. createEnclosure to fabryka zwracająca obiekt wybiegu z nazwą, pojemnością i pustą listą dinozaurów:
1describe('DinosaurEnclosure', () => {
2 it('should have correct capacity', () => {
3 const enclosure = createEnclosure('T-Rex Zone', 5);
4 expect(enclosure.capacity).toBe(5);
5 });
6
7 it('should start empty', () => {
8 const enclosure = createEnclosure('Raptor Pen', 10);
9 expect(enclosure.dinosaurs).toEqual([]);
10 expect(enclosure.dinosaurs.length).toBe(0);
11 });
12});Gdy asercja zawiedzie, Jest przerywa tylko ten jeden test, pokazuje wartość oczekiwaną (Expected) i otrzymaną (Received), po czym przechodzi do następnego. Dlaczego dla pustej tablicy użyliśmy toEqual, a nie toBe, wyjaśnię w lekcji o matcherach.
Kompletny przykład
Zobaczmy pełny przykład testowania modułu zarządzania dinozaurami. Najpierw kod produkcyjny: createDinosaur tworzy okaz z pełnym zdrowiem, a feedDinosaur podnosi zdrowie tylko wtedy, gdy pokarm pasuje do diety, i nigdy powyżej 100:
1// dinosaur.js - kod produkcyjny
2function createDinosaur(name, species, diet) {
3 return {
4 name,
5 species,
6 diet,
7 health: 100,
8 isAlive: true,
9 };
10}
11
12function feedDinosaur(dinosaur, food) {
13 if (dinosaur.diet === 'carnivore' && food === 'meat') {
14 dinosaur.health = Math.min(100, dinosaur.health + 20);
15 return { success: true, message: 'Dinozaur najedzony!' };
16 }
17 if (dinosaur.diet === 'herbivore' && food === 'plants') {
18 dinosaur.health = Math.min(100, dinosaur.health + 15);
19 return { success: true, message: 'Dinozaur najedzony!' };
20 }
21 return { success: false, message: 'Niewłaściwe jedzenie!' };
22}Teraz plik z testami. Dwa zagnieżdżone describe odpowiadają dwóm funkcjom, a każdy it sprawdza jeden scenariusz:
1// dinosaur.test.js - testy
2describe('Dinosaur Management', () => {
3 describe('createDinosaur', () => {
4 it('should create a dinosaur with correct properties', () => {
5 const rex = createDinosaur('Rex', 'T-Rex', 'carnivore');
6
7 expect(rex.name).toBe('Rex');
8 expect(rex.species).toBe('T-Rex');
9 expect(rex.diet).toBe('carnivore');
10 expect(rex.health).toBe(100);
11 expect(rex.isAlive).toBe(true);
12 });
13 });
14
15 describe('feedDinosaur', () => {
16 it('should feed carnivore with meat', () => {
17 const rex = createDinosaur('Rex', 'T-Rex', 'carnivore');
18 rex.health = 70;
19
20 const result = feedDinosaur(rex, 'meat');
21
22 expect(result.success).toBe(true);
23 expect(rex.health).toBe(90);
24 });
25
26 it('should reject wrong food for carnivore', () => {
27 const rex = createDinosaur('Rex', 'T-Rex', 'carnivore');
28
29 const result = feedDinosaur(rex, 'plants');
30
31 expect(result.success).toBe(false);
32 });
33 });
34});W prawdziwym projekcie plik testów importuje funkcje, np. przez require('./dinosaur'), a dinosaur.js musi je wyeksportować. Zauważ też lukę: test odrzuconego pokarmu nie sprawdza, czy zdrowie Rexa zostało nietknięte. To dobry kandydat na Twój pierwszy samodzielny test.
Uruchamianie testów
Testy uruchamiasz z terminala. Oto cztery warianty, których będziesz używać najczęściej:
1# Uruchom wszystkie testy
2npx jest
3
4# Uruchom konkretny plik
5npx jest dinosaur.test.js
6
7# Uruchom w trybie watch (reaguje na zmiany)
8npx jest --watch
9
10# Uruchom z raportowaniem pokrycia kodu
11npx jest --coverageTryb --watch po każdym zapisie uruchamia ponownie tylko testy związane ze zmienionymi plikami (zmiany wykrywa przez Gita), a --coverage tworzy raport pokrycia: pokazuje, które linie kodu wykonał choć jeden test, a których nie odwiedził żaden.
Wzorzec AAA (Arrange-Act-Assert)
Każdy dobry test powinien być napisany według wzorca AAA: Arrange (przygotuj), Act (wykonaj), Assert (sprawdź). Trzy części oddzielone pustą linią sprawiają, że test czyta się jak raport z inspekcji:
1it('should increase health when fed correctly', () => {
2 // Arrange (Przygotowanie) - stwórz potrzebne obiekty
3 const dino = createDinosaur('Brachio', 'Brachiosaurus', 'herbivore');
4 dino.health = 60;
5
6 // Act (Akcja) - wykonaj testowaną operację
7 const result = feedDinosaur(dino, 'plants');
8
9 // Assert (Sprawdzenie) - zweryfikuj wynik
10 expect(result.success).toBe(true);
11 expect(dino.health).toBe(75);
12});Kod produkcyjny się nie zmienił - zmienił się tylko porządek testu, dzięki któremu od razu widać, co jest przygotowaniem, a co właściwym sprawdzeniem. Moja rada: jedna akcja w sekcji Act. Jeśli wywołujesz tam trzy różne funkcje, prawdopodobnie piszesz trzy testy naraz.
W następnej lekcji poznasz cały arsenał matcherów. W laboratorium poniżej działa uproszczona symulacja describe, it i expect napisana w czystym JavaScripcie, bo Sandpack nie uruchamia prawdziwego Jesta.
Pamiętaj: describe to wybieg, it to pojedyncza inspekcja, a expect to czujnik, który melduje PASS albo FAIL.
Kod do tej lekcji: index.js
1// Jest - Podstawy frameworka testowego
2console.log("=== Park Jurajski - System Testowania Jest ===\n");
3
4// Symulacja frameworka Jest (Sandpack nie uruchamia prawdziwego Jest)
5// Pokaz struktury: describe, it/test, expect
6
7// --- Mini implementacja Jest ---
8const results = { passed: 0, failed: 0, suites: [] };
9
10function describe(name, fn) {
11 const suite = { name, tests: [] };
12 results.suites.push(suite);
13 const prevSuite = results.currentSuite;
14 results.currentSuite = suite;
15 fn();
16 results.currentSuite = prevSuite;
17}
18
19function it(name, fn) {
20 try {
21 fn();
22 results.currentSuite.tests.push({ name, status: "PASS" });
23 results.passed++;
24 } catch (e) {
25 results.currentSuite.tests.push({ name, status: "FAIL", error: e.message });
26 results.failed++;
27 }
28}
29const test = it; // test i it sa zamienne
30
31function expect(actual) {
32 return {
33 toBe(expected) {
34 if (actual !== expected)
35 throw new Error(`Expected ${expected}, got ${actual}`);
36 },
37 toEqual(expected) {
38 if (JSON.stringify(actual) !== JSON.stringify(expected))
39 throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
40 },
41 toHaveLength(expected) {
42 if (actual.length !== expected)
43 throw new Error(`Expected length ${expected}, got ${actual.length}`);
44 },
45 toBeTruthy() {
46 if (!actual) throw new Error(`Expected truthy, got ${actual}`);
47 },
48 not: {
49 toBe(expected) {
50 if (actual === expected) throw new Error(`Expected not ${expected}`);
51 }
52 }
53 };
54}
55
56// --- Kod produkcyjny ---
57function createDinosaur(name, species, diet) {
58 return { name, species, diet, health: 100, isAlive: true };
59}
60
61function feedDinosaur(dinosaur, food) {
62 if (dinosaur.diet === "carnivore" && food === "meat") {
63 dinosaur.health = Math.min(100, dinosaur.health + 20);
64 return { success: true, message: "Dinozaur najedzony!" };
65 }
66 if (dinosaur.diet === "herbivore" && food === "plants") {
67 dinosaur.health = Math.min(100, dinosaur.health + 15);
68 return { success: true, message: "Dinozaur najedzony!" };
69 }
70 return { success: false, message: "Niewlasciwe jedzenie!" };
71}
72
73// --- Testy ---
74describe("Dinosaur Management", () => {
75 describe("createDinosaur", () => {
76 it("should create a dinosaur with correct properties", () => {
77 const rex = createDinosaur("Rex", "T-Rex", "carnivore");
78 expect(rex.name).toBe("Rex");
79 expect(rex.species).toBe("T-Rex");
80 expect(rex.diet).toBe("carnivore");
81 expect(rex.health).toBe(100);
82 expect(rex.isAlive).toBe(true);
83 });
84
85 it("should create herbivore dinosaur", () => {
86 const brachio = createDinosaur("Brachio", "Brachiosaurus", "herbivore");
87 expect(brachio.diet).toBe("herbivore");
88 expect(brachio.health).toBe(100);
89 });
90 });
91
92 describe("feedDinosaur", () => {
93 it("should feed carnivore with meat (AAA pattern)", () => {
94 // Arrange
95 const rex = createDinosaur("Rex", "T-Rex", "carnivore");
96 rex.health = 70;
97
98 // Act
99 const result = feedDinosaur(rex, "meat");
100
101 // Assert
102 expect(result.success).toBe(true);
103 expect(rex.health).toBe(90);
104 });
105
106 it("should reject wrong food for carnivore", () => {
107 const rex = createDinosaur("Rex", "T-Rex", "carnivore");
108 const result = feedDinosaur(rex, "plants");
109 expect(result.success).toBe(false);
110 });
111
112 it("should feed herbivore with plants", () => {
113 const brachio = createDinosaur("Brachio", "Brachiosaurus", "herbivore");
114 brachio.health = 50;
115 const result = feedDinosaur(brachio, "plants");
116 expect(result.success).toBe(true);
117 expect(brachio.health).toBe(65);
118 });
119 });
120});
121
122// --- Wyswietl wyniki ---
123console.log("Test Results:\n");
124results.suites.forEach(suite => {
125 console.log(` ${suite.name}`);
126 suite.tests.forEach(t => {
127 const icon = t.status === "PASS" ? "PASS" : "FAIL";
128 console.log(` [${icon}] ${t.name}`);
129 if (t.error) console.log(` ${t.error}`);
130 });
131});
132console.log(`\nTotal: ${results.passed} passed, ${results.failed} failed`);Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co oznacza wzorzec AAA stosowany w testach jednostkowych?
2. Jaki jest cel funkcji expect() w Jest?
Zadania praktyczne w grze
- Edytor kodu
Uzupełnij brakujące asercje w testach dla funkcji getDinosaurSpecies.
- Klikanie w kolejności
Ułóż elementy podstawowego testu Jest:
- Edytor kodu
Napisz testy dla systemu punktów życia dinozaura używając describe, it, expect.