Kurs JavaScript i TypeScript · Moduł 11: Testowanie z Jest
Mockowanie w Jest
W tej lekcji5
System bezpieczeństwa parku po wykryciu naruszenia wysyła SMS do strażników. Test takiego systemu nie może naprawdę wysyłać wiadomości przy każdym uruchomieniu - byłby wolny, zależny od sieci, a strażnicy szybko przestaliby reagować na alarmy. Potrzebujemy dublera: czegoś, co wygląda jak prawdziwa funkcja, ale niczego nie wysyła, za to notuje, kto i z czym go wywołał.
Mockowanie to tworzenie sztucznych wersji funkcji, modułów lub obiektów. W Parku Jurajskim to jak tworzenie modeli dinozaurów do testów bezpieczeństwa zamiast używania prawdziwych bestii.
jest.fn() - tworzenie mock funkcji
jest.fn() tworzy "pustą" funkcję, która rejestruje wszystkie wywołania. Domyślnie zwraca undefined, a do sprawdzania jej historii służą matchery z rodziny toHaveBeenCalled:
1test('jest.fn() - podstawy', () => {
2 // Tworzenie mock funkcji
3 const feedDino = jest.fn();
4
5 // Wywołanie mock funkcji
6 feedDino('Rex', 'meat');
7 feedDino('Brachio', 'plants');
8
9 // Sprawdzanie wywołań
10 expect(feedDino).toHaveBeenCalled();
11 expect(feedDino).toHaveBeenCalledTimes(2);
12 expect(feedDino).toHaveBeenCalledWith('Rex', 'meat');
13 expect(feedDino).toHaveBeenLastCalledWith('Brachio', 'plants');
14});toHaveBeenCalled pyta, czy funkcja w ogóle zadziałała, toHaveBeenCalledTimes - ile razy, toHaveBeenCalledWith - czy choć raz dostała te argumenty, a toHaveBeenLastCalledWith - z czym ją wywołano ostatnio. Mock nikogo nie nakarmił, tylko zapisał, że próbowano.
Mock z implementacją
Możesz nadać mockowi zachowanie: mockReturnValue ustawia wartość zwracaną przy każdym wywołaniu, mockReturnValueOnce tylko przy jednym, a funkcja przekazana do jest.fn staje się pełną implementacją:
1test('mock z implementacją', () => {
2 // Mock z wartością zwracaną
3 const getHealth = jest.fn().mockReturnValue(100);
4 expect(getHealth()).toBe(100);
5
6 // Mock z różnymi wartościami dla kolejnych wywołań
7 const checkSensor = jest.fn()
8 .mockReturnValueOnce('OK')
9 .mockReturnValueOnce('WARNING')
10 .mockReturnValueOnce('DANGER');
11
12 expect(checkSensor()).toBe('OK');
13 expect(checkSensor()).toBe('WARNING');
14 expect(checkSensor()).toBe('DANGER');
15
16 // Mock z własną implementacją
17 const calculateDamage = jest.fn((attack, defense) => {
18 return Math.max(0, attack - defense);
19 });
20
21 expect(calculateDamage(50, 30)).toBe(20);
22 expect(calculateDamage(10, 30)).toBe(0);
23});Łańcuch mockReturnValueOnce odtwarza scenariusz krok po kroku: czujnik melduje OK, potem ostrzeżenie, a na końcu zagrożenie. Gdy wartości jednorazowe się wyczerpią, mock wraca do mockReturnValue, a jeśli go nie ustawiono, do undefined. Dla funkcji asynchronicznych istnieją odpowiedniki: mockResolvedValue zwraca obietnicę spełnioną podaną wartością, a mockRejectedValue obietnicę odrzuconą.
jest.mock() - mockowanie modułów
jest.mock() pozwala zastąpić cały moduł mockiem. Najpierw spójrz na moduł, którego nie chcemy uruchamiać w testach - sendAlert wysyła alert przez fetch do API parku:
1// Plik: alertService.js
2export function sendAlert(zone, message) {
3 // W rzeczywistości wysyła email/SMS - nie chcemy tego w testach!
4 return fetch('/api/alerts', {
5 method: 'POST',
6 body: JSON.stringify({ zone, message })
7 });
8}W pliku testowym wywołanie jest.mock('./alertService') zastępuje wszystkie funkcje tego modułu mockami zwracającymi undefined, a require pobiera już podmienioną wersję. SecuritySystem to testowana klasa, która wewnętrznie korzysta z alertService:
1// Plik: securitySystem.test.js
2jest.mock('./alertService');
3
4const { sendAlert } = require('./alertService');
5
6describe('SecuritySystem', () => {
7 beforeEach(() => {
8 // Resetuj mocki przed każdym testem
9 sendAlert.mockClear();
10 sendAlert.mockResolvedValue({ status: 'sent' });
11 });
12
13 it('should send alert on breach', async () => {
14 const system = new SecuritySystem();
15 await system.reportBreach('Zone A');
16
17 expect(sendAlert).toHaveBeenCalledWith(
18 'Zone A',
19 expect.stringContaining('naruszenie')
20 );
21 });
22});Test sprawdza, że system poprosił o wysłanie alertu, ale żaden SMS nie wyszedł. expect.stringContaining to matcher asymetryczny: akceptuje każdy string zawierający podany fragment. Zwróć też uwagę na kolejność: z babel-jest wywołania jest.mock są automatycznie przenoszone na początek pliku (hoisting), więc mock zadziała, nawet gdy import stoi wyżej.
jest.mock z fabryką
Automatyczny mock nie zawsze wystarcza. Drugi argument jest.mock to fabryka: funkcja zwracająca obiekt, który udaje moduł. Masz wtedy pełną kontrolę nad tym, co moduł eksportuje:
1jest.mock('./alertService', () => ({
2 sendAlert: jest.fn().mockResolvedValue({ status: 'sent' }),
3}));Tym razem sendAlert od początku zwraca spełnioną obietnicę, więc konfiguracja w beforeEach przestaje być potrzebna. Hoisting ma jednak konsekwencję: fabryka nie może używać zmiennych zadeklarowanych poza nią, chyba że ich nazwa zaczyna się od słowa mock.
jest.spyOn() - szpiegowanie metod
spyOn monitoruje istniejącą metodę bez jej zastępowania (domyślnie). Różnica jest zasadnicza: jest.fn tworzy nową, pustą funkcję, a jest.spyOn owija metodę, która już istnieje w obiekcie, i nadal wywołuje oryginał:
1test('spyOn - monitorowanie metody', () => {
2 const park = {
3 dinosaurs: [],
4 addDinosaur(dino) {
5 this.dinosaurs.push(dino);
6 return this.dinosaurs.length;
7 },
8 getDinosaurCount() {
9 return this.dinosaurs.length;
10 }
11 };
12
13 // Szpieguj metodę - zachowuje oryginalną implementację
14 const spy = jest.spyOn(park, 'addDinosaur');
15
16 park.addDinosaur({ name: 'Rex' });
17 park.addDinosaur({ name: 'Blue' });
18
19 // Sprawdź wywołania
20 expect(spy).toHaveBeenCalledTimes(2);
21 expect(spy).toHaveBeenCalledWith({ name: 'Rex' });
22
23 // Oryginalna funkcja nadal działa!
24 expect(park.dinosaurs).toHaveLength(2);
25
26 // Przywróć oryginalną implementację
27 spy.mockRestore();
28});Szpieg zanotował oba wywołania, a dinozaury i tak trafiły do parku, bo oryginalna metoda zadziałała. mockRestore na końcu zdejmuje szpiega i przywraca metodę sprzed testu. Działa tylko dla mocków utworzonych przez spyOn.
spyOn z podmianą implementacji
Czasem chcesz podsłuchać metodę i jednocześnie zablokować jej prawdziwe działanie, bo na przykład łączy się z siecią. Wtedy do spyOn dopinasz mockImplementation. Podmieniona metoda zwraca obietnicę, więc test jest async i czeka na wynik przez await:
1test('spyOn - podmiana implementacji', async () => {
2 const api = {
3 fetchDinoData(id) {
4 // Normalne wywołanie API - nie chcemy tego w testach
5 return fetch(`/api/dinosaurs/${id}`);
6 }
7 };
8
9 // Zastąp implementację na czas testu
10 const spy = jest.spyOn(api, 'fetchDinoData').mockImplementation((id) => {
11 return Promise.resolve({
12 id,
13 name: 'Mock Rex',
14 species: 'T-Rex'
15 });
16 });
17
18 // Teraz fetchDinoData zwraca mock data
19 const data = await api.fetchDinoData('001');
20 expect(data.name).toBe('Mock Rex');
21
22 spy.mockRestore();
23});Gdybyś zamiast await dopisał samo .then(...) bez return, test zakończyłby się przed spełnieniem obietnicy i zostałby zaliczony, zanim asercja w ogóle się wykona. Tej pułapce przyjrzymy się dokładnie w następnej lekcji.
Sprawdzanie wywołań mock
Jest udostępnia bogaty zestaw matcherów dla mocków. Każdy mock ma też właściwość mock.calls: tablicę, w której każdy element to lista argumentów jednego wywołania:
1test('matchery dla mocków', () => {
2 const callback = jest.fn();
3
4 callback('Rex');
5 callback('Blue', 'raptor');
6
7 // Czy został wywołany?
8 expect(callback).toHaveBeenCalled();
9
10 // Ile razy?
11 expect(callback).toHaveBeenCalledTimes(2);
12
13 // Z jakimi argumentami?
14 expect(callback).toHaveBeenCalledWith('Rex');
15 expect(callback).toHaveBeenNthCalledWith(1, 'Rex');
16 expect(callback).toHaveBeenNthCalledWith(2, 'Blue', 'raptor');
17
18 // Dostęp do szczegółów wywołań
19 expect(callback.mock.calls).toHaveLength(2);
20 expect(callback.mock.calls[0]).toEqual(['Rex']);
21 expect(callback.mock.calls[1]).toEqual(['Blue', 'raptor']);
22});toHaveBeenNthCalledWith numeruje wywołania od 1, a mock.calls to zwykła tablica indeksowana od 0, stąd różne numery w tym samym teście. Matchery dają czytelniejsze komunikaty błędów, więc po mock.calls sięgaj tylko w nietypowych sytuacjach.
Sprzątanie po mockach
Mock pamięta wywołania także między testami. Dlatego w przykładzie z alertService hook beforeEach wywołuje mockClear, który czyści historię wywołań. mockReset robi to samo i dodatkowo usuwa ustawione zachowanie, a mockRestore przywraca jeszcze oryginalną metodę po spyOn. Dla wszystkich mocków naraz działają jest.clearAllMocks, jest.resetAllMocks i jest.restoreAllMocks, a opcja clearMocks: true w konfiguracji czyści historię automatycznie przed każdym testem.
Moja rada: mockuj granice systemu, czyli sieć, bazę danych, zegar i SMS-y, a nie własną logikę. Jeśli podmienisz wszystko, test sprawdzi tylko, czy mocki działają. W następnej lekcji mockResolvedValue spotka się z testowaniem kodu asynchronicznego, a w laboratorium poniżej zobaczysz, jak od środka działa uproszczony jest.fn().
Pamiętaj: mock to dubler dinozaura na próbie bezpieczeństwa - wygląda jak prawdziwy, nie gryzie i dokładnie notuje każdy ruch.
Kod do tej lekcji: index.js
1// Mockowanie - jest.fn(), jest.mock(), spyOn
2console.log("=== Park Jurajski - Klonowanie i Mockowanie ===\n");
3
4// Symulacja jest.fn()
5function createMockFn(impl) {
6 const mock = {
7 calls: [],
8 results: [],
9 returnValues: [],
10 implementations: impl ? [impl] : [],
11 };
12
13 function mockFn(...args) {
14 mock.calls.push(args);
15 let result;
16 if (mock.returnValues.length > 0) {
17 result = mock.returnValues.shift();
18 } else if (mock.implementations.length > 0) {
19 result = mock.implementations[0](...args);
20 } else {
21 result = undefined;
22 }
23 mock.results.push(result);
24 return result;
25 }
26
27 mockFn.mock = mock;
28 mockFn.mockReturnValue = (val) => {
29 mock.implementations = [() => val];
30 return mockFn;
31 };
32 mockFn.mockReturnValueOnce = (val) => {
33 mock.returnValues.push(val);
34 return mockFn;
35 };
36 mockFn.mockImplementation = (fn) => {
37 mock.implementations = [fn];
38 return mockFn;
39 };
40 mockFn.mockClear = () => {
41 mock.calls = [];
42 mock.results = [];
43 };
44
45 return mockFn;
46}
47
48// --- jest.fn() basics ---
49console.log("--- jest.fn() - tworzenie mock funkcji ---\n");
50
51const feedDino = createMockFn();
52feedDino("Rex", "meat");
53feedDino("Brachio", "plants");
54
55console.log("Wywolania:", feedDino.mock.calls);
56console.log("Liczba wywolan:", feedDino.mock.calls.length);
57console.log("Pierwsze wywolanie:", feedDino.mock.calls[0]);
58console.log("Drugie wywolanie:", feedDino.mock.calls[1]);
59
60// --- Mock z wartoscia zwracana ---
61console.log("\n--- Mock z wartoscia zwracana ---\n");
62
63const getHealth = createMockFn();
64getHealth.mockReturnValue(100);
65console.log("getHealth():", getHealth());
66console.log("getHealth():", getHealth());
67
68// --- Mock z roznymi wartosciami ---
69console.log("\n--- Mock z roznymi wartosciami (mockReturnValueOnce) ---\n");
70
71const checkSensor = createMockFn();
72checkSensor.mockReturnValueOnce("OK");
73checkSensor.mockReturnValueOnce("WARNING");
74checkSensor.mockReturnValueOnce("DANGER");
75
76console.log("Odczyt 1:", checkSensor());
77console.log("Odczyt 2:", checkSensor());
78console.log("Odczyt 3:", checkSensor());
79
80// --- Mock z implementacja ---
81console.log("\n--- Mock z implementacja ---\n");
82
83const calculateDamage = createMockFn((attack, defense) => {
84 return Math.max(0, attack - defense);
85});
86
87console.log("damage(50, 30):", calculateDamage(50, 30));
88console.log("damage(10, 30):", calculateDamage(10, 30));
89console.log("Wywolania:", calculateDamage.mock.calls);
90
91// --- spyOn - szpiegowanie metod ---
92console.log("\n--- spyOn - monitorowanie istniejacych metod ---\n");
93
94const park = {
95 dinosaurs: [],
96 addDinosaur(dino) {
97 this.dinosaurs.push(dino);
98 return this.dinosaurs.length;
99 }
100};
101
102// Symulacja spyOn
103const originalAdd = park.addDinosaur.bind(park);
104const spyCalls = [];
105park.addDinosaur = function(...args) {
106 spyCalls.push(args);
107 return originalAdd(...args);
108};
109
110park.addDinosaur({ name: "Rex" });
111park.addDinosaur({ name: "Blue" });
112
113console.log("Spy wywolania:", spyCalls.length);
114console.log("Pierwsze wywolanie:", JSON.stringify(spyCalls[0]));
115console.log("Dinozaurow w parku:", park.dinosaurs.length);
116console.log("(Oryginalna funkcja nadal dziala!)");
117
118// --- Matchery dla mockow ---
119console.log("\n--- Sprawdzanie wywolan mock ---\n");
120const callback = createMockFn();
121callback("Rex");
122callback("Blue", "raptor");
123
124console.log("Czy wywolany?", callback.mock.calls.length > 0 ? "TAK" : "NIE");
125console.log("Ile razy?", callback.mock.calls.length);
126console.log("Wszystkie wywolania:", JSON.stringify(callback.mock.calls));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 robi jest.fn() w Jest?
2. Jaka jest główna różnica między jest.fn() a jest.spyOn()?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Użyj mock funkcji do testowania systemu alarmowego parku.
- Układanie w poziomie
Ułóż elementy konfiguracji mock return value:
- Edytor kodu
Monitoruj metody systemu parku za pomocą spyOn i sprawdź wywołania.