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

Unit Testing - testowanie poszczególnych elementów fortu

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, @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.

Podsumowanie

Machina rozłożona, każda część zbadana osobno:

  • test jednostkowy bada najmniejszy element w izolacji od reszty systemu,
  • describe
    grupuje testy jednego elementu,
    it
    opisuje jedno zachowanie,
    expect
    stawia oczekiwanie,
  • nazwa w
    it
    mó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ł,
  • 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,
  • 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.

Przejdź do CodeWorlds