Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Mocking - symulacja warunków bojowych

Chcesz przetestować serwis, który wypłaca żołd. Sęk w tym, że sięga on do bazy danych i do zewnętrznego skarbca Imperium. Test wymagałby więc działającej bazy, sieci i cudzego serwera - byłby wolny, a padłby przy pierwszej awarii łącza, choć Twój kod byłby bez zarzutu.

Legion ćwiczy inaczej. Na placu manewrowym stawia się kukły zamiast prawdziwych przeciwników. Kukła nie oddaje ciosów, ale pozwala sprawdzić, czy legionista zachowuje szyk. W testach takie kukły nazywamy test doubles - dublerami.

Cztery rodzaje kukieł

Dublery różnią się tym, ile potrafią. Warto znać całą czwórkę, bo nazwy wracają w każdej dokumentacji - ułożone od najprostszej do najbardziej złożonej:

  1. Dummy - kukła wypchana słomą. Wypełnia miejsce parametru, którego metoda i tak nie użyje. Nie robi nic.
  2. Stub - kukła z gotową odpowiedzią. Zapytana, zawsze mówi to samo, bez cienia logiki. "Legionista o id 1? Oto Marek."
  3. Spy - kukła, która zapamiętuje ciosy. Odpowiada jak stub, ale dodatkowo zapisuje, ile razy ją zawołano i z czym.
  4. Mock - kukła z oczekiwaniami. Nie tylko zapisuje wywołania, ale służy do weryfikowania interakcji: czy serwis naprawdę zawołał repozytorium, i to dokładnie raz.

W praktyce Jest zaciera granicę między trzema ostatnimi - ta sama funkcja bywa stubem i spy zarazem. Ale rozróżnienie ról zostaje: stub dostarcza dane, spy obserwuje, mock weryfikuje.

jest.fn() - kukła zbudowana od zera

Podstawowe narzędzie tworzy pustą funkcję-atrapę:

1const findOne = jest.fn();

Ta funkcja nic nie robi i zwraca

undefined
, ale Jest śledzi każde jej wywołanie - zapamiętuje, ile razy padła i z jakimi argumentami. Samo
jest.fn()
jest więc już szpiegiem; brakuje mu tylko odpowiedzi.

Odpowiedź dokładamy jedną z czterech metod, które różnią się tym, co zwracają:

1const repo = {
2  findOne: jest.fn().mockResolvedValue({ id: 1, name: 'Marek' }),
3  count: jest.fn().mockReturnValue(42),
4  save: jest.fn().mockRejectedValue(new Error('Skarbiec niedostępny')),
5};

mockReturnValue
zwraca wartość od razu, synchronicznie - nadaje się do metod, które nie są asynchroniczne.
mockResolvedValue
zwraca spełnioną obietnicę, więc pasuje wszędzie tam, gdzie kod robi
await
.
mockRejectedValue
zwraca obietnicę odrzuconą - to nim sprawdzisz, czy Twój serwis poprawnie obsługuje awarię.

Jest też wariant z przyrostkiem

Once
:
mockReturnValueOnce
odpowiada tak tylko za pierwszym razem, a potem wraca do zachowania domyślnego. Przydaje się, gdy chcesz, by pierwsze wywołanie się nie powiodło, a ponowienie już tak.

Tak przygotowaną kukłę wstawiamy w miejsce prawdziwej zależności:

1const module = await Test.createTestingModule({
2  providers: [
3    PayService,
4    { provide: getRepositoryToken(Legionary), useValue: repo },
5  ],
6}).compile();

useValue
mówi NestJS: gdy ktoś poprosi o repozytorium legionistów, podaj mu ten obiekt zamiast prawdziwego. Serwis niczego nie zauważy - dostanie coś, co ma te same metody.

jest.spyOn() - szpieg przy istniejącej metodzie

Czasem nie chcesz budować kukły od zera, tylko podejrzeć prawdziwy obiekt. Do tego służy druga metoda:

1const spy = jest.spyOn(legionService, 'findAll');

Różnica między nimi jest zasadnicza i o nią właśnie pytają najczęściej.

jest.fn()
tworzy nową funkcję, która nigdzie wcześniej nie istniała.
jest.spyOn()
bierze istniejącą metodę istniejącego obiektu
i owija ją obserwacją.

Kluczowe jest to, czego

spyOn
domyślnie nie robi: nie podmienia zachowania. Prawdziwa metoda nadal się wykonuje, a Ty tylko widzisz, że ją zawołano. Gdy chcesz również podstawić odpowiedź, dokładasz ją tak samo jak wcześniej:

1jest.spyOn(legionService, 'findAll').mockResolvedValue([]);

Teraz prawdziwa metoda już się nie wykona. Ta para - obserwuj albo obserwuj i zastąp - to cały

spyOn
.

Weryfikacja - czy kukła została zawołana

Skoro dublery zapisują wywołania, możemy o nie zapytać w asercjach:

1expect(spy).toHaveBeenCalledTimes(1);
2expect(repo.findOne).toHaveBeenCalledWith({ where: { id: 1 } });

toHaveBeenCalledTimes
sprawdza liczbę wywołań,
toHaveBeenCalledWith
- argumenty. To właśnie w tym momencie dubler pełni rolę mocka: nie interesuje nas już, co zwrócił, tylko czy rozmowa w ogóle się odbyła i w jakiej formie.

Uważaj jednak, żeby nie przesadzić. Testowanie każdego wywołania wiąże test z wewnętrzną budową serwisu - drobny refaktor zepsuje wtedy testy, choć zachowanie kodu się nie zmieniło. Sprawdzaj interakcje tam, gdzie sama interakcja jest celem: że e-mail został wysłany, że zapis do bazy nastąpił. Dla zwykłych odczytów wystarczy sprawdzić wynik.

Sprzątanie po ćwiczeniach

Dublery pamiętają wywołania - także te z poprzedniego testu. Bez czyszczenia

toHaveBeenCalledTimes(1)
zacznie w drugim teście widzieć dwa wywołania:

1afterEach(() => {
2  jest.clearAllMocks();
3});

clearAllMocks
zeruje liczniki i zapisane argumenty wszystkich atrap. To jedna z tych linijek, których brak objawia się dopiero wtedy, gdy testy zaczynają przechodzić lub padać zależnie od kolejności uruchomienia - a to najtrudniejszy rodzaj usterki do wyśledzenia. Wstaw ją od razu, @name.

Podsumowanie

Plac manewrowy gotowy, kukły ustawione:

  • test doubles zastępują prawdziwe zależności, dzięki czemu test nie potrzebuje bazy ani sieci,
  • czwórka od najprostszej: Dummy (wypełniacz), Stub (gotowa odpowiedź), Spy (zapamiętuje wywołania), Mock (weryfikuje interakcje),
  • jest.fn()
    tworzy nową funkcję-atrapę i od razu śledzi jej wywołania,
  • odpowiedzi:
    mockReturnValue
    synchronicznie,
    mockResolvedValue
    spełniona obietnica,
    mockRejectedValue
    odrzucona, warianty
    ...Once
    działają tylko raz,
  • useValue
    w
    createTestingModule
    podstawia atrapę zamiast prawdziwej zależności,
  • jest.spyOn()
    obserwuje istniejącą metodę i domyślnie nie zmienia jej zachowania - dopiero
    mockResolvedValue
    je podmienia,
  • toHaveBeenCalledTimes
    sprawdza liczbę wywołań,
    toHaveBeenCalledWith
    - argumenty,
  • nie weryfikuj każdego wywołania: test przywiązany do wewnętrznej budowy pęka przy refaktorze,
  • jest.clearAllMocks()
    w
    afterEach
    zeruje liczniki - bez tego testy zaczną zależeć od kolejności.

W następnej lekcji sprawdzimy, ile z kodu naprawdę pokryły Twoje testy - poznasz test coverage. A na razie zapamiętaj: dubler to kukła na placu manewrowym;

jest.fn()
ją buduje,
jest.spyOn()
przebiera za nią kogoś prawdziwego, a asercje pytają, czy legionista w ogóle zadał cios.

Przejdź do CodeWorlds