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

Mockowanie w Jest

6 min czytania
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. 1. Co robi jest.fn() w Jest?

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

Przydatne artykuły