Kurs NestJS · Moduł 7: Testowanie
Unit Testing - testowanie poszczególnych elementów fortu
W tej lekcji6
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.
Trzy słowa, z których zbudowany jest test
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.
Problem: serwis nie powstaje przez new
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.
Trzy kroki modułu testowego
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: Test.createTestingModule({...}) tworzy izolowany moduł na potrzeby testu. W 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: .compile() finalizuje konfigurację - NestJS rozwiązuje zależności między providerami i tworzy ich instancje. To operacja asynchroniczna, stąd await. Bez tego wywołania dostajesz jedynie opis modułu, nie działający moduł.
Krok trzeci: module.get(LegionsService) wyjmuje z gotowego modułu instancję providera - tę samą, którą NestJS zbudował, z już wstrzykniętą atrapą repozytorium. Zapis <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.
Świeży fort przed każdą próbą
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ą.
Jeden test, jedno zachowanie
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. 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.
Podsumowanie
Machina rozłożona, każda część zbadana osobno:
- test jednostkowy bada najmniejszy element w izolacji od reszty systemu,
describegrupuje testy jednego elementu,itopisuje jedno zachowanie,expectstawia oczekiwanie,- nazwa w
itmówi, co element ma robić - to ona pojawi się w raporcie po awarii, - serwisu NestJS nie tworzymy przez
new, bo ma wstrzykiwane zależności - służy do tego moduł testowy, - trzy kroki:
Test.createTestingModule({providers})buduje izolowany moduł,.compile()finalizuje go i tworzy instancje,module.get(Serwis)wyjmuje gotowy provider, - pominięcie
.compile()to najczęstszy błąd - bez niego masz opis modułu, nie moduł, useValuepodmienia prawdziwą zależność na atrapę, dzięki czemu test nie dotyka bazy,beforeEachdaje świeżą instancję przed każdym testem - bez tego wynik zaczyna zależeć od kolejności uruchomienia,- jedno zachowanie na test: test badający pięć rzeczy naraz nie mówi, która się zepsuła.
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.
Kod do tej lekcji: src/unit-testing.spec.ts
1// Unit Testing - Testowanie Poszczegolnych Elementow Fortu
2import { Test, TestingModule } from '@nestjs/testing';
3
4// Serwis do testowania - zarzadzanie prowincjami
5class ProvinceService {
6 private provinces = [
7 { id: 1, name: 'Gallia', governor: 'Caesar', tribute: 1000 },
8 { id: 2, name: 'Aegyptus', governor: 'Cleopatra', tribute: 2000 },
9 { id: 3, name: 'Britannia', governor: 'Agricola', tribute: 500 },
10 ];
11
12 findAll() {
13 return this.provinces;
14 }
15
16 findOne(id: number) {
17 return this.provinces.find(p => p.id === id) || null;
18 }
19
20 calculateTotalTribute(): number {
21 return this.provinces.reduce((sum, p) => sum + p.tribute, 0);
22 }
23
24 getProvincesByGovernor(governor: string) {
25 return this.provinces.filter(p => p.governor === governor);
26 }
27}
28
29describe('ProvinceService - Unit Tests', () => {
30 let service: ProvinceService;
31
32 // beforeEach - przygotowanie przed kazdym testem
33 beforeEach(() => {
34 service = new ProvinceService();
35 });
36
37 describe('findAll', () => {
38 it('should return all provinces', () => {
39 const provinces = service.findAll();
40 expect(provinces).toHaveLength(3);
41 expect(provinces[0].name).toBe('Gallia');
42 });
43 });
44
45 describe('findOne', () => {
46 it('should return a province by id', () => {
47 const province = service.findOne(1);
48 expect(province).toBeDefined();
49 expect(province!.name).toBe('Gallia');
50 });
51
52 it('should return null for non-existent id', () => {
53 const province = service.findOne(999);
54 expect(province).toBeNull();
55 });
56 });
57
58 describe('calculateTotalTribute', () => {
59 it('should calculate total tribute correctly', () => {
60 const total = service.calculateTotalTribute();
61 expect(total).toBe(3500); // 1000 + 2000 + 500
62 });
63 });
64
65 describe('getProvincesByGovernor', () => {
66 it('should filter provinces by governor', () => {
67 const provinces = service.getProvincesByGovernor('Caesar');
68 expect(provinces).toHaveLength(1);
69 expect(provinces[0].name).toBe('Gallia');
70 });
71
72 it('should return empty array for unknown governor', () => {
73 const provinces = service.getProvincesByGovernor('Nero');
74 expect(provinces).toHaveLength(0);
75 });
76 });
77});
78Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Test.createTestingModule() w NestJS służy do:
2. Co robi metoda compile() w TestingModule?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Stwórz konfigurację TestingModule z wstrzykniętymi zależnościami
- Układanie w poziomie
Ułóż tworzenie TestingModule w poprawnej kolejności: