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

Matchery w Jest

6 min czytania
W tej lekcji6

Samo "czy wynik się zgadza" to często za mało. Czasem chcesz wiedzieć, czy lista gatunków zawiera raptora, czy zdrowie mieści się w normie albo czy funkcja podniosła alarm. Do każdego z tych pytań Jest ma osobne narzędzie.

Matchery to metody, które pozwalają sprawdzać wartości na różne sposoby. W kontekście Parku Jurajskiego - to różne typy sensorów, z których każdy wykrywa inny rodzaj zagrożenia. Dopisujesz je po kropce za expect(...), a zasada wyboru jest prosta: bierz najbardziej precyzyjny czujnik, bo wtedy Jest wypisze najczytelniejszy komunikat błędu.

Podstawowe matchery porównujące

toBe - ścisłe porównanie (===)

toBe używa ścisłego porównania. Sprawdza identyczność - idealny dla typów prymitywnych. Dokładniej: Jest porównuje wartości funkcją Object.is, która działa jak === z dwoma wyjątkami - NaN jest równe NaN, a 0 i -0 są rozróżniane:

1test('toBe - porównanie prymitywów', () => {
2  const dinoCount = 15;
3  expect(dinoCount).toBe(15);      // liczba
4
5  const species = 'T-Rex';
6  expect(species).toBe('T-Rex');    // string
7
8  const isAlive = true;
9  expect(isAlive).toBe(true);       // boolean
10});

Każda z trzech asercji porównuje prymityw: liczbę, tekst i wartość logiczną. Dla nich liczy się wyłącznie sama wartość, więc toBe jest tu najprostszym wyborem.

toEqual - porównanie strukturalne

toEqual sprawdza równość strukturalną obiektów. Porównuje zawartość, nie referencje. To ważne, bo dwa obiekty o identycznej treści to dwa osobne okazy w pamięci, więc dla toBe zawsze będą różne:

1test('toEqual - porównanie obiektów', () => {
2  const dino1 = { name: 'Rex', species: 'T-Rex' };
3  const dino2 = { name: 'Rex', species: 'T-Rex' };
4
5  // toBe FAIL - różne referencje (obiekty w pamięci)
6  // expect(dino1).toBe(dino2); // To nie zadziała!
7
8  // toEqual PASS - ta sama zawartość
9  expect(dino1).toEqual(dino2); // To zadziała!
10
11  // Działa też z zagnieżdżonymi obiektami
12  const enclosure = {
13    name: 'Zone A',
14    dinosaurs: [
15      { name: 'Rex', diet: 'carnivore' }
16    ]
17  };
18
19  expect(enclosure).toEqual({
20    name: 'Zone A',
21    dinosaurs: [
22      { name: 'Rex', diet: 'carnivore' }
23    ]
24  });
25});

toEqual schodzi rekurencyjnie w głąb zagnieżdżonych obiektów i tablic, pole po polu. Jest przy tym pobłażliwy: ignoruje pola o wartości undefined i nie sprawdza, czy obiekt jest instancją tej samej klasy. Gdy te szczegóły mają znaczenie, użyj toStrictEqual - ja sięgam po niego zawsze, gdy nie mam powodu do pobłażliwości.

Matchery dla tablic i stringów

toContain - czy zawiera element?

Gdy nie interesuje Cię cała lista, tylko obecność jednego okazu, użyj toContain. Działa zarówno z tablicami, jak i ze stringami:

1test('toContain - sprawdzanie zawartości', () => {
2  const dinosaurs = ['T-Rex', 'Velociraptor', 'Triceratops'];
3
4  expect(dinosaurs).toContain('T-Rex');
5  expect(dinosaurs).toContain('Velociraptor');
6
7  // Dla stringów
8  const report = 'Dinozaur T-Rex w strefie A';
9  expect(report).toContain('T-Rex');
10  expect(report).toContain('strefie A');
11});

Dla tablicy toContain sprawdza, czy któryś element jest ściśle równy szukanemu, a dla stringa, czy zawiera podany fragment. Uwaga na obiekty: toContain porównuje referencje, więc do szukania obiektu po zawartości służy toContainEqual.

toHaveLength - sprawdzanie długości

Liczba okazów na wybiegu to kolejne częste pytanie. toHaveLength sprawdza właściwość length, więc zadziała z tablicami i ze stringami:

1test('toHaveLength - sprawdzanie ilości', () => {
2  const carnivores = ['T-Rex', 'Velociraptor', 'Spinosaurus'];
3  expect(carnivores).toHaveLength(3);
4
5  const emptyEnclosure = [];
6  expect(emptyEnclosure).toHaveLength(0);
7
8  // Działa też ze stringami
9  const code = 'DINO-001';
10  expect(code).toHaveLength(8);
11});

Zapis expect(carnivores).toHaveLength(3) daje ten sam wynik co expect(carnivores.length).toBe(3), ale przy porażce Jest pokaże całą tablicę, a nie tylko liczbę. Pusta tablica ma długość 0 i to również poprawne sprawdzenie.

Matchery dla wartości prawdziwych/fałszywych

toBeTruthy i toBeFalsy

W JavaScripcie każda wartość w warunku if zamienia się na true albo false. toBeTruthy i toBeFalsy sprawdzają właśnie wynik tej konwersji, a nie konkretną wartość:

1test('sprawdzanie prawdziwości', () => {
2  // Truthy values
3  expect(1).toBeTruthy();
4  expect('text').toBeTruthy();
5  expect([]).toBeTruthy();      // pusta tablica jest truthy!
6  expect({}).toBeTruthy();      // pusty obiekt jest truthy!
7
8  // Falsy values
9  expect(0).toBeFalsy();
10  expect('').toBeFalsy();
11  expect(null).toBeFalsy();
12  expect(undefined).toBeFalsy();
13  expect(false).toBeFalsy();
14});

Pułapka czyha w pierwszej grupie: pusta tablica i pusty obiekt są truthy. Jeśli chcesz sprawdzić, że wybieg jest pusty, toBeFalsy nie pomoże - użyj toHaveLength(0).

toBeNull, toBeUndefined, toBeDefined

Zakładamy, że getDinosaur zwraca obiekt dinozaura albo null, gdy okazu nie ma w rejestrze. Trzy matchery rozróżniają trzy różne stany braku wartości:

1test('sprawdzanie null i undefined', () => {
2  const activeDino = getDinosaur('Rex-001');
3  const extinctDino = getDinosaur('Unknown');
4
5  expect(activeDino).toBeDefined();
6  expect(activeDino).not.toBeNull();
7
8  expect(extinctDino).toBeNull();
9  expect(extinctDino).toBeDefined(); // null jest defined!
10
11  let uninitializedDino;
12  expect(uninitializedDino).toBeUndefined();
13});

toBeDefined oznacza tylko "różne od undefined", dlatego null przechodzi ten test. Jeśli chcesz wykluczyć oba puste stany naraz, sprawdź konkretną wartość, zamiast polegać na toBeDefined.

Matchery liczbowe

Czujniki parku zwracają liczby: zdrowie, napięcie ogrodzenia, temperaturę inkubatora. Do porównań służą matchery, których nazwy czyta się jak zwykłe zdania po angielsku:

1test('porównania liczbowe', () => {
2  const health = 85;
3
4  expect(health).toBeGreaterThan(80);
5  expect(health).toBeGreaterThanOrEqual(85);
6  expect(health).toBeLessThan(100);
7  expect(health).toBeLessThanOrEqual(85);
8
9  // Dla liczb zmiennoprzecinkowych - NIE używaj toBe!
10  expect(0.1 + 0.2).toBeCloseTo(0.3);
11});

Ostatnia linia to ważna lekcja o liczbach zmiennoprzecinkowych: 0.1 + 0.2 daje w JavaScripcie 0.30000000000000004, więc toBe(0.3) by nie przeszło. toBeCloseTo domyślnie porównuje z dokładnością do dwóch miejsc po przecinku.

Testowanie wyjątków - toThrow

toThrow sprawdza, czy funkcja rzuca wyjątek. UWAGA: musisz opakować wywołanie w funkcję! Bez opakowania błąd wybuchnie, zanim expect zdąży go złapać. W przykładzie releaseDinosaur wypuszcza okaz tylko przy odpowiednim poziomie zabezpieczeń:

1function releaseDinosaur(dinosaur, securityLevel) {
2  if (securityLevel < 5) {
3    throw new Error('Poziom bezpieczeństwa za niski!');
4  }
5  if (dinosaur.species === 'T-Rex' && securityLevel < 9) {
6    throw new Error('T-Rex wymaga poziomu bezpieczeństwa 9!');
7  }
8  return { released: true };
9}
10
11test('toThrow - testowanie wyjątków', () => {
12  const rex = { name: 'Rex', species: 'T-Rex' };
13
14  // POPRAWNIE - opakuj w funkcję strzałkową
15  expect(() => releaseDinosaur(rex, 3)).toThrow();
16  expect(() => releaseDinosaur(rex, 3)).toThrow('Poziom bezpieczeństwa za niski!');
17  expect(() => releaseDinosaur(rex, 7)).toThrow('T-Rex wymaga poziomu bezpieczeństwa 9!');
18
19  // Sprawdź, że NIE rzuca wyjątku
20  expect(() => releaseDinosaur(rex, 9)).not.toThrow();
21});

Argument toThrow zawęża sprawdzenie: string oznacza, że komunikat błędu musi zawierać ten fragment, wyrażenie regularne - że musi do niego pasować, a klasa błędu - że wyjątek musi być jej instancją. Polecam zawsze podawać komunikat bo samo toThrow() przepuści każdy wyjątek, także TypeError spowodowany literówką.

Modyfikator not

Każdy matcher można zanegować za pomocą .not. Przydaje się, gdy chcesz potwierdzić nieobecność, na przykład że stegozaur nie trafił na wybieg drapieżników:

1test('modyfikator not', () => {
2  const dinosaurs = ['T-Rex', 'Triceratops'];
3
4  expect(dinosaurs).not.toContain('Stegosaurus');
5  expect(dinosaurs.length).not.toBe(0);
6  expect(dinosaurs).not.toEqual([]);
7
8  const health = 85;
9  expect(health).not.toBeLessThan(50);
10});

not odwraca wynik dowolnego matchera, ale nie nadużywaj go: not.toBeLessThan(50) czyta się trudniej niż toBeGreaterThanOrEqual(50). Sięgaj po not tam, gdzie zdanie "nie zawiera" brzmi naturalnie.

W lekcji o mockowaniu poznasz matchery, które sprawdzają wywołania funkcji, a przy TypeScripcie - matchery asymetryczne, które weryfikują sam kształt obiektu. W laboratorium poniżej przetestujesz uproszczoną wersję expect z tymi samymi matcherami.

Pamiętaj: matcher to czujnik - dobierz taki, który wykrywa dokładnie to zagrożenie, którego szukasz.

Kod do tej lekcji: index.js
1// Matchery w Jest - rozne typy sprawdzen
2console.log("=== Park Jurajski - Sensory i Matchery ===\n");
3
4// Mini expect z wieloma matcherami
5let passed = 0, failed = 0;
6
7function expect(actual) {
8  function check(condition, msg) {
9    if (condition) { passed++; console.log("  [PASS] " + msg); }
10    else { failed++; console.log("  [FAIL] " + msg); }
11  }
12
13  return {
14    toBe(expected) {
15      check(actual === expected, `${actual} === ${expected}`);
16    },
17    toEqual(expected) {
18      check(JSON.stringify(actual) === JSON.stringify(expected),
19        `deepEqual: ${JSON.stringify(actual)}`);
20    },
21    toContain(item) {
22      const has = Array.isArray(actual) ? actual.includes(item) : actual.includes(item);
23      check(has, `contains "${item}"`);
24    },
25    toHaveLength(len) {
26      check(actual.length === len, `length ${actual.length} === ${len}`);
27    },
28    toBeTruthy() {
29      check(!!actual, `${actual} is truthy`);
30    },
31    toBeFalsy() {
32      check(!actual, `${actual} is falsy`);
33    },
34    toBeGreaterThan(n) {
35      check(actual > n, `${actual} > ${n}`);
36    },
37    toBeLessThan(n) {
38      check(actual < n, `${actual} < ${n}`);
39    },
40    toBeCloseTo(expected) {
41      check(Math.abs(actual - expected) < 0.01, `${actual} ~= ${expected}`);
42    },
43    toBeNull() {
44      check(actual === null, `${actual} is null`);
45    },
46    toBeUndefined() {
47      check(actual === undefined, `is undefined`);
48    },
49    toBeDefined() {
50      check(actual !== undefined, `is defined`);
51    },
52    not: {
53      toBe(expected) { check(actual !== expected, `${actual} !== ${expected}`); },
54      toContain(item) {
55        const has = Array.isArray(actual) ? actual.includes(item) : actual.includes(item);
56        check(!has, `not contains "${item}"`);
57      },
58      toThrow() { /* simplified */ }
59    }
60  };
61}
62
63// --- toBe vs toEqual ---
64console.log("--- toBe vs toEqual ---");
65
66const dino1 = { name: "Rex", species: "T-Rex" };
67const dino2 = { name: "Rex", species: "T-Rex" };
68
69console.log("toBe porownuje referencje (===):");
70console.log("  dino1 === dino2?", dino1 === dino2, "(false - rozne obiekty)");
71console.log("\ntoEqual porownuje zawartosc:");
72expect(dino1).toEqual(dino2);
73
74// --- toContain ---
75console.log("\n--- toContain ---");
76const dinosaurs = ["T-Rex", "Velociraptor", "Triceratops"];
77expect(dinosaurs).toContain("T-Rex");
78expect(dinosaurs).not.toContain("Stegosaurus");
79
80const report = "Dinozaur T-Rex w strefie A";
81expect(report).toContain("T-Rex");
82
83// --- toHaveLength ---
84console.log("\n--- toHaveLength ---");
85const carnivores = ["T-Rex", "Velociraptor", "Spinosaurus"];
86expect(carnivores).toHaveLength(3);
87
88// --- Truthy / Falsy ---
89console.log("\n--- Truthy / Falsy ---");
90expect(1).toBeTruthy();
91expect("text").toBeTruthy();
92expect(0).toBeFalsy();
93expect("").toBeFalsy();
94expect(null).toBeFalsy();
95
96// --- Liczby ---
97console.log("\n--- Porownania liczbowe ---");
98const health = 85;
99expect(health).toBeGreaterThan(80);
100expect(health).toBeLessThan(100);
101expect(0.1 + 0.2).toBeCloseTo(0.3);
102
103// --- null / undefined ---
104console.log("\n--- null / undefined ---");
105expect(null).toBeNull();
106expect(undefined).toBeUndefined();
107expect("value").toBeDefined();
108
109// --- toThrow ---
110console.log("\n--- toThrow ---");
111function releaseDinosaur(dino, level) {
112  if (level < 5) throw new Error("Poziom za niski!");
113  return { released: true };
114}
115
116try {
117  releaseDinosaur({}, 3);
118  console.log("  [FAIL] Should have thrown");
119  failed++;
120} catch (e) {
121  console.log("  [PASS] Threw: " + e.message);
122  passed++;
123}
124
125try {
126  releaseDinosaur({}, 9);
127  console.log("  [PASS] No throw for level 9");
128  passed++;
129} catch (e) {
130  console.log("  [FAIL] Should not throw");
131  failed++;
132}
133
134console.log(`\n=== Wyniki: ${passed} passed, ${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. Jaka jest główna różnica między matcherami toBe i toEqual w Jest?

  2. 2. Jak poprawnie użyć matchera toThrow w Jest?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Dla każdego testu użyj właściwego matchera: toBe, toContain, toHaveLength, toBeGreaterThan, toEqual.

  • Układanie w poziomie

    Ułóż elementy asercji porównującej obiekt:

  • Edytor kodu

    Napisz testy sprawdzające zarówno pozytywne jak i negatywne warunki.

Przydatne artykuły