Rozdział 9: Testy jednostkowe
Rozdział 9: Testy jednostkowe – Streszczenie praktyczne
O co chodzi w tej sekcji
Główną tezą autora jest to, że kod testowy jest tak samo ważny jak kod produkcyjny i musi być utrzymywany w najwyższej czystości, ponieważ "brudne" testy są gorsze niż ich brak. Tylko zestaw czystych testów jednostkowych daje programiście pewność niezbędną do bezkarnego zmieniania i czyszczenia kodu produkcyjnego – bez nich każda modyfikacja staje się potencjalnym błędem, co prowadzi do degradacji systemu.
Kluczowe zasady i reguły
- Trzy prawa TDD:
- Nie wolno pisać kodu produkcyjnego bez wcześniej napisanego testu, który nie przechodzi.
- Nie wolno pisać więcej testu jednostkowego, niż potrzeba, by nie przeszedł (brak kompilacji to też błąd).
- Nie wolno pisać więcej kodu produkcyjnego, niż wystarczy do zaliczenia bieżącego testu.
- Czytelność przede wszystkim: To najważniejsza cecha czystego testu. Powinien on być klarowny, prosty i spójny, aby czytelnik mógł błyskawicznie zrozumieć intencję sprawdzanej logiki.
- Wzorzec BUILD-OPERATE-CHECK: Struktura testu powinna być podzielona na trzy wyraźne fazy: budowanie danych testowych, wykonanie operacji na tych danych i sprawdzenie wyników.
- Tworzenie domenowego języka testowania (DSL): Zamiast bezpośrednio wywoływać skomplikowane API systemowe, warto tworzyć metody pomocnicze (wrappery), które ukrywają techniczny szum i pozwalają czytać test jak opowieść o logice biznesowej.
- Podwójny standard wydajności: Kod testowy musi być czysty, ale nie musi być tak wydajny jak produkcyjny. Można w nim stosować mniej optymalne rozwiązania (np. łączenie napisów zamiast
StringBuffer), jeśli tylko poprawia to czytelność. - Jedna koncepcja na test: Unikaj długich metod testowych sprawdzających wiele rzeczy po kolei. Każdy test powinien skupiać się na jednym konkretnym aspekcie działania.
- Zasady F.I.R.S.T.:
- Fast (Szybkie): Testy muszą działać błyskawicznie, inaczej programiści nie będą ich uruchamiać.
- Independent (Niezależne): Testy nie mogą zależeć od siebie nawzajem ani od kolejności uruchomienia.
- Repeatable (Powtarzalne): Test musi dawać ten sam wynik w każdym środowisku (u programisty, na serwerze, w pociągu bez sieci).
- Self-Validating (Samokontrolujące się): Test zwraca zero-jedynkowy wynik (sukces/porażka); nie wymaga czytania logów przez człowieka.
- Timely (O czasie): Testy piszemy bezpośrednio przed kodem produkcyjnym.
Przykłady kodu
Przed refaktoryzacją: Test przeładowany szczegółami technicznymi Poniższy kod jest trudny do zrozumienia, ponieważ zawiera mnóstwo szumu związanego z API FitNesse, rzutowaniem typów i ręcznym budowaniem żądań.
public void testGetPageHieratchyAsXml() throws Exception {
crawler.addPage(root, PathParser.parse("PageOne"));
crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
crawler.addPage(root, PathParser.parse("PageTwo"));
request.setResource("root");
request.addInput("type", "pages");
Responder responder = new SerializedPageResponder();
SimpleResponse response =
(SimpleResponse) responder.makeResponse(
new FitNesseContext(root), request);
String xml = response.getContent();
assertEquals("text/xml", response.getContentType());
assertSubString("<name>PageOne</name>", xml);
assertSubString("<name>PageTwo</name>", xml);
assertSubString("<name>ChildOne</name>", xml);
}
Po refaktoryzacji: Czysty test korzystający z domenowego API
Dzięki wydzieleniu metod pomocniczych (makePages, submitRequest), test stał się czytelny i wyraźnie pokazuje intencje autora.
public void testGetPageHierarchyAsXml() throws Exception {
makePages("PageOne", "PageOne.ChildOne", "PageTwo");
submitRequest("root", "type:pages");
assertResponseIsXML();
assertResponseContains(
"<name>PageOne</name>", "<name>PageTwo</name>", " <name>ChildOne</name>"
);
}
Przykład "Podwójnego Standardu": Czytelność ponad wydajność W testach systemów wbudowanych autor zamiast sprawdzania każdego czujnika osobno, używa krótkiego ciągu znaków do reprezentacji stanu, co jest mniej wydajne, ale znacznie łatwiejsze do skanowania wzrokiem.
// Podejście tradycyjne - trudne do szybkiego odczytania
assertTrue(hw.heaterState());
assertTrue(hw.blowerState());
assertFalse(hw.coolerState());
assertFalse(hw.hiTempAlarm());
assertTrue(hw.loTempAlarm());
// Podejście z podwójnym standardem - asercja na zwięzłym stanie
assertEquals("HBchL", hw.getState());
// H=heater, B=blower, c=cooler(off), h=hiAlarm(off), L=loAlarm
Praktyczne wnioski
- Utrzymuj testy tak samo rygorystycznie jak produkcję. Jeśli pozwolisz na bałagan w testach, ich utrzymanie stanie się tak drogie, że zespół w końcu przestanie je pisać, co doprowadzi do błędu "brodzenia" w kiepskim kodzie.
- Stosuj refaktoryzację w obu kierunkach. Po doprowadzeniu testu do "zielonego", nie przechodź od razu do następnego zadania. Poświęć czas na wyczyszczenie kodu testowego, który właśnie napisałeś.
- Wyeliminuj techniczny szum. Jeśli Twój test zaczyna się od 20 linii ustawiania mocków i bazy danych, wydziel to do metody
setUplub dedykowanej klasy fabrykującej. - Nie bój się długich nazw w testach. Metody testowe powinny być precyzyjne – nazwa
testSymbolicLinksAreNotInXmlPageHierarchyjest znacznie lepsza niżtestLinks. - Dąż do minimalizacji asercji. Nie musisz kurczowo trzymać się zasady "jednej asercji na test", ale pilnuj, by test sprawdzał tylko jedną koncepcję biznesową.
- Zadbaj o niezależność od środowiska. Jeśli test przechodzi u Ciebie, ale wywala się na serwerze CI (lub odwrotnie), nie ignoruj tego – to sygnał o ukrytych sprzężeniach, które zemszczą się w przyszłości.