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

Framework Jest - podstawy

6 min czytania
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@latest

Pierwsza 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 --coverage

Tryb --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. 1. Co oznacza wzorzec AAA stosowany w testach jednostkowych?

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

Przydatne artykuły