Unit tests: szybka diagnoza przy kodzie
- data
- kategoria
- Testing
- czytanie
- 3 min / 606 słów
Najprostszy błąd w kodzie nie powinien czekać na staging.
Jeżeli funkcja źle liczy rabat, walidator przepuszcza pusty email albo mapper gubi jedno pole, to najtańsze miejsce na wykrycie błędu jest tuż obok kodu.
To jest miejsce dla unit test.
Co to za rodzaj testu
Unit test sprawdza małą jednostkę zachowania w izolacji od reszty systemu.
Jednostką nie musi być jedna funkcja.
Może nią być:
funkcja
metoda
klasa
mały moduł
reducer
validator
mapper
policy object
Ważne jest nie to, jak nazywa się plik, tylko czy test ma krótki dystans do decyzji w kodzie.
Jeżeli asercja pada, powinno być jasne, który warunek, branch albo mapping przestał działać.
Unit test nie powinien potrzebować prawdziwej bazy, sieci, brokera wiadomości, przeglądarki ani całego frameworka aplikacji.
Może używać mock, stub, fake albo fixture, ale tylko wtedy, kiedy to upraszcza granicę testu.
Jeżeli połowa testu opisuje fałszywy świat dookoła kodu, to znak, że jednostka jest źle wybrana albo kod jest zbyt mocno sklejony z zależnościami.
Czego unit test nie udowadnia
Unit test nie mówi, że system działa.
Mówi:
ta decyzja w kodzie daje oczekiwany wynik dla tego wejścia
To jest mała obietnica, ale bardzo wartościowa.
Bez unit testów proste błędy logiki uciekają wyżej.
Wtedy test E2E potrafi paść dlatego, że jedna funkcja źle zaokrągliła liczbę.
Diagnoza zaczyna się od przeglądarki, requestu, fixture użytkownika i logów, chociaż poprawka siedzi w trzech linijkach kodu.
Unit test skraca tę pętlę.
Izolacja nie znaczy testowanie implementacji
Najgorszy unit test zna za dużo środka.
Sprawdza, że metoda prywatna została wywołana dwa razy.
Wiąże się z kolejnością kroków, która nie jest częścią zachowania.
Pęka po refactorze, mimo że wynik nadal jest poprawny.
Lepszy unit test patrzy na wejście i wyjście jednostki.
dane wejściowe
stan początkowy
wywołanie publicznej funkcji
oczekiwany wynik
Jeżeli trzeba sprawdzić interakcję z zależnością, test powinien sprawdzać kontrakt tej interakcji, nie każdy ruch wewnątrz funkcji.
Na przykład: czy kod wysyła event OrderPaid, a nie czy najpierw zawołał buildPayload(), potem serialize(), a potem publish().
Mocks, stubs i fakes
Mock bywa potrzebny, ale łatwo robi z testu zapis implementacji.
W praktyce warto rozróżniać kilka prostych rzeczy.
Stub zwraca przygotowaną odpowiedź.
Fake jest prostą implementacją zależności, na przykład magazynem w pamięci.
Mock zwykle sprawdza, czy zależność została wywołana w określony sposób.
Im więcej mocków w teście, tym częściej test opisuje połączenia między obiektami zamiast zachowania.
To nie znaczy, że mocki są złe.
Znaczy, że trzeba uważać, gdzie leży wartość testu.
Jeżeli test z mockami łapie zmianę publicznej decyzji, jest użyteczny.
Jeżeli łapie wyłącznie zmianę kolejności prywatnych kroków, będzie przeszkadzał przy refactorze.
Unit test jako test regresji
Dobry unit test często powstaje po błędzie.
Najpierw jest objaw:
użytkownik z kuponem 100% dostaje ujemną cenę
Potem poprawka.
Potem unit test, który zamyka dokładnie ten przypadek.
Wtedy unit test staje się jednocześnie regression test.
Nazwa unit mówi o poziomie izolacji.
Nazwa regression mówi, dlaczego test istnieje.
To są różne osie nazewnictwa i warto ich nie mieszać.
Narzędzie ze stacka
Do unit tests w tym stacku wybrałbym Vitest.
Pasuje do TypeScriptu, Reacta i backendu w Node, więc można mieć ten sam model pracy po obu stronach aplikacji.
Na froncie dobrze łączy się z funkcjami pomocniczymi, reducerami, walidatorami i hookami, które nie wymagają pełnego renderowania komponentu.
Na backendzie sprawdza reguły domenowe, parsery, mappery, policy objects i małe serwisy bez odpalania Fastify ani bazy.
Jeżeli projekt ma już Jest, nie przepisywałbym testów tylko dla nazwy narzędzia.
Mechanizm jest ważniejszy niż runner.
Kiedy unit test jest za mały
Unit test przestaje wystarczać, kiedy ryzyko leży na granicy.
Nie sprawdzi, czy SQL pasuje do schematu bazy.
Nie sprawdzi, czy serializer i parser rozumieją ten sam format.
Nie sprawdzi, czy aplikacja ma poprawnie podpięty middleware.
Nie sprawdzi, czy transakcja obejmuje wszystkie zapisy.
Wtedy potrzebny jest kolejny typ testu.
Nie dlatego, że unit test był zły.
Dlatego, że odpowiedział na swoje pytanie i doszedł do granicy, której z definicji nie widzi.
Następna warstwa to component tests.