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.
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:
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.
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.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.
tworzy nową funkcję, która nigdzie wcześniej nie istniała. jest.fn()
bierze istniejącą metodę istniejącego obiektu i owija ją obserwacją.jest.spyOn()
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.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.
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.Plac manewrowy gotowy, kukły ustawione:
jest.fn() tworzy nową funkcję-atrapę i od razu śledzi jej wywołania,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,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.