Katapulta nie strzela, ale winowajcy nie widać. Może pękła lina, może zawiódł mechanizm zwalniający, może źle policzono przeciwwagę. Sprawdzanie całej machiny naraz mówi tylko tyle, że coś nie działa - nie mówi, co.
Mechanik legionowy postępuje inaczej: rozkłada machinę i bada każdą część osobno. Lina wytrzymuje napięcie? Mechanizm zwalnia przy pociągnięciu? Test jednostkowy robi dokładnie to samo z kodem - sprawdza najmniejszy element w izolacji od reszty systemu.
Zacznijmy od najprostszego przypadku - klasy bez żadnych zależności:
1describe('Bolt', () => {
2 it('oblicza wagę pocisku', () => {
3 const bolt = new Bolt('iron', 5);
4
5 expect(bolt.getWeight()).toBe(5);
6 });
7});Trzy słowa i już masz test.
describe grupuje testy dotyczące jednego elementu - to nagłówek rozdziału. it opisuje jedno zachowanie, a jego nazwa czyta się jako zdanie: "it oblicza wagę pocisku". expect stawia oczekiwanie i porównuje je z rzeczywistością, tutaj przez dopasowanie toBe.Zwróć uwagę na nazwę w
it: mówi ona, co element ma robić, a nie jak jest zbudowany. Gdy taki test padnie w raporcie CI, przeczytasz "oblicza wagę pocisku" i od razu wiesz, co się zepsuło - bez otwierania pliku.Klasę
Bolt stworzyliśmy zwykłym new. Ale serwis NestJS ma wstrzykiwane zależności - repozytorium, konfigurację, inne serwisy. Napisanie new LegionsService() skończy się błędem, bo konstruktor czegoś oczekuje.Można by podać te zależności ręcznie, ale wtedy omijamy cały mechanizm wstrzykiwania i test przestaje przypominać rzeczywistość. Dlatego NestJS daje moduł testowy: miniaturową wersję aplikacji, zawierającą wyłącznie to, co potrzebne do jednego testu.
Budowa modułu to zawsze ta sama sekwencja:
1const module = await Test.createTestingModule({
2 providers: [
3 LegionsService,
4 { provide: getRepositoryToken(Legion), useValue: repoMock },
5 ],
6}).compile();
7
8const service = module.get<LegionsService>(LegionsService);Krok pierwszy:
tworzy izolowany moduł na potrzeby testu. W Test.createTestingModule({...})
providers wyliczasz tylko to, czego test naprawdę potrzebuje - badany serwis i jego zależności. Prawdziwe repozytorium podmieniamy tu na atrapę przez useValue, dzięki czemu test nie dotyka bazy.Krok drugi:
finalizuje konfigurację - NestJS rozwiązuje zależności między providerami i tworzy ich instancje. To operacja asynchroniczna, stąd .compile()
await. Bez tego wywołania dostajesz jedynie opis modułu, nie działający moduł.Krok trzeci:
wyjmuje z gotowego modułu instancję providera - tę samą, którą NestJS zbudował, z już wstrzykniętą atrapą repozytorium. Zapis module.get(LegionsService)
<LegionsService> to parametr typu; dzięki niemu TypeScript wie, co dostajesz, i podpowie metody.Zapamiętaj tę trójkę jako jedno: zbuduj, skompiluj, wyjmij. Pominięcie
compile() to najczęstszy błąd przy pierwszym module testowym.Powyższy kod trafia zwykle do haka
beforeEach:1describe('LegionsService', () => {
2 let service: LegionsService;
3 let repoMock: { find: jest.Mock };
4
5 beforeEach(async () => {
6 repoMock = { find: jest.fn() };
7
8 const module = await Test.createTestingModule({
9 providers: [
10 LegionsService,
11 { provide: getRepositoryToken(Legion), useValue: repoMock },
12 ],
13 }).compile();
14
15 service = module.get<LegionsService>(LegionsService);
16 });
17});beforeEach uruchamia się przed każdym testem, więc każdy dostaje świeżą instancję serwisu i czystą atrapę. Po co, skoro budowanie modułu kosztuje?Bo bez tego testy zaczynają na siebie wpływać. Serwis, który w jednym teście zapisał coś w pamięci, poniesie ten stan do następnego - a wtedy wynik zaczyna zależeć od kolejności uruchomienia. Taki test bywa zielony u Ciebie i czerwony w CI, gdzie Jest zrównolegla pracę inaczej. Świeża instancja usuwa całą tę klasę problemów jedną linijką.
Mając moduł, piszemy właściwe testy:
1it('zwraca listę legionów', async () => {
2 repoMock.find.mockResolvedValue([{ id: 1, name: 'Legio X' }]);
3
4 const result = await service.findAll();
5
6 expect(result).toHaveLength(1);
7});
8
9it('zwraca pustą listę, gdy brak legionów', async () => {
10 repoMock.find.mockResolvedValue([]);
11
12 const result = await service.findAll();
13
14 expect(result).toEqual([]);
15});Dwa zachowania, dwa testy - i to jest reguła, którą polecam trzymać sztywno, @name. Kusi, by sprawdzić oba przypadki w jednym
it, ale wtedy tracisz najcenniejszą własność testów jednostkowych: gdy padnie, wiesz dokładnie co. Test badający pięć rzeczy naraz mówi tyle samo co niedziałająca katapulta - coś jest nie tak, szukaj sam.Zauważ też, czego te testy nie sprawdzają: czy repozytorium naprawdę pobiera dane z bazy. To celowe - atrapa odcina bazę, więc badamy wyłącznie logikę serwisu. Współpracę prawdziwych elementów sprawdzimy innym rodzajem testów.
Machina rozłożona, każda część zbadana osobno:
describe grupuje testy jednego elementu, it opisuje jedno zachowanie, expect stawia oczekiwanie,it mówi, co element ma robić - to ona pojawi się w raporcie po awarii,new, bo ma wstrzykiwane zależności - służy do tego moduł testowy,Test.createTestingModule({providers}) buduje izolowany moduł, .compile() finalizuje go i tworzy instancje, module.get(Serwis) wyjmuje gotowy provider,.compile() to najczęstszy błąd - bez niego masz opis modułu, nie moduł,useValue podmienia prawdziwą zależność na atrapę, dzięki czemu test nie dotyka bazy,beforeEach daje świeżą instancję przed każdym testem - bez tego wynik zaczyna zależeć od kolejności uruchomienia,W następnej lekcji sprawdzimy to, czego test jednostkowy z założenia nie widzi - czy elementy naprawdę współpracują ze sobą. A na razie zapamiętaj: buduj, skompiluj, wyjmij - a potem badaj jedną część naraz.