Unit testing. Praktyczny przewodnik z przykładami z życia

Unit testing. Praktyczny przewodnik z przykładami z życia
Porządkujemy pytania i pokazujemy praktyczne techniki, które realnie wspierają jakość kodu z perspektywy testera. Dlaczego testy jednostkowe są potrzebne?

Wielu testerów na średnim etapie kariery ma już za sobą pierwsze zetknięcia z testami jednostkowymi. Być może analizowali istniejące testu w projektach, czasem coś poprawiali, a może nawet pisali nowe, ale wciąż robili to z poczuciem, że temat nie został do końca oswojony. Czy każdy test jednostkowy musi być napisany przez programistę? Czy test add(2, 3) == 5 ma jakikolwiek sens? I co właściwie znaczy „dobry” test jednostkowy?

Na początek warto odpowiedzieć na pytanie, które często pojawia się w zespole, zwłaszcza gdy testy jednostkowe traktowane są jak przykry obowiązek: po co w ogóle je pisać, skoro aplikacja działa? Otóż testy jednostkowe mają zupełnie inną funkcję niż testy integracyjne czy eksploracyjne. Ich głównym celem nie jest wykrywanie defektów, lecz stworzenie środowiska, w którym zmiana kodu nie jest ryzykownym eksperymentem. To właśnie testy jednostkowe dają pewność, że zachowanie modułu nie uległo nieświadomej zmianie. Pozwalają bezpiecznie refaktoryzować, dokumentują wymagania biznesowe (często lepiej niż specyfikacja) i przyspieszają debugowanie poprzez izolację systemu. 

Jak napisać test, który coś naprawdę testuje?

Efektywne testy jednostkowe nie biorą się z przypadku. Muszą być dobrze napisane. W praktyce oznacza to stosowanie struktury, w której setup i cleanup są jasno oddzielone od właściwego testy. W JUnit 5 służą do tego adnotacje takie jak @BeforeEach, @AfterEach, czy @BeforeAll. Ważne jest, by każdy test weryfikował jedno zachowanie, np. wynik działania metody albo rzucenie wyjątku w określonej sytuacji. Sercem testu są asercje, których nie powinno zabraknąć – assertEquals, assertTrue, assertThrows, a także bardziej rozbudowane z biblioteki AssertJ, które pozwalają pisać czytelniejsze, bardziej opisowe warunki.

Częstym błędem jest testowanie jak działa kod, a nie co robi. Jeśli test łamie klęka się po zmianie implementacji, mimo że funkcjonalność nie uległa zmianie, to znaczy, że został źle zaprojektowany. Testy powinny odzwierciedlać zachowanie oczekiwane przez użytkownika, a nie sposób, w jaki jest ono zaimplementowane.

Jak skutecznie mockować zależności?

W przypadku bardziej złożonych klas i serwisów pojawia się kolejny aspekt: zależności. Gdy testujemy komponent, który korzysta z innych klas, nie chcemy, by cała logika zewnętrzna wpływała na wynik testu. Tu z pomocą przychodzi Mockito. Mocki pozwalają odizolować testowaną jednostkę od otoczenia. Możemy zdefiniować, że metoda charge () zawsze zwróci true, niezależnie od realnej logiki. Dzięki adnotacjom @Mock, @InjectMocks@Captor możliwe jest przechwytywanie danych i sprawdzanie, czy testowana klasa wywołuje metody zależności tak, jak oczekujemy. Co istotne, można również używać @Spy, gdy chcemy testować realny obiekt, ale kontrolować i weryfikować część jego zachowań. 

W praktyce nie chodzi o to, by mockować wszystko. Wręcz przeciwnie, mockować należy tylko to, co znajduje się poza zakresem odpowiedzi testowanej jednostki. Często są to np. klasy infrastrukturalne (logowanie, połączenia sieciowe), ale niekoniecznie zależności domenowe.

Czego unikać w testach jednostkowych?

Wielu testerów popełnia też inne, powtarzalne błędy. Zdarza się, że testy są nieczytelne, mają nieintuicyjne nazwy, nie wiadomo, co właściwie testują, albo są zbyt ogólne. JUnit umożliwia nadawanie testom opisów przy pomocy @DisplayName, co znacząco zwiększa czytelność raportów testów. Równie częsty problem to dublowanie podobnych przypadków. Zamiast pisać trzy testy dla różnych danych wejściowych, warto sięgnąć po testy parametryzowane z @CsvSource lub @MethodSource, które pozwalają przetestować wiele przypadków w jednej metodzie, w sposób przejrzysty i skalowalny. 

Testy jednostkowe powinny być także szybkie i niezależne. Jeśli test zależy od innego testu, to nie mamy już do czynienia z testem jednostkowym. Gdy test długo się wykonuje, warto sprawdzić, czy nie testujemy zbyt szerokiego zakresu. Można też ustawić limit czasu za pomocą adnotacji @Timeout, co pozwala kontrolować, czy dana operacja nie przekracza oczekiwanego czasu wykonania. 

Jak zwiększyć elastyczność i czytelność testów?

W projektach, gdzie testujemy złożone obiekty, przydają się techniki tzw. soft assertions, czyli takich, które nie zatrzymują testu przy pierwszym błędzie. AssertJ pozwala na użycie AutoCloseableSoftAssertions, a JUnit na grupowanie asercji za pomocą assertAll. To szczególnie przydatne w testach walidacyjnych, gdzie chcemy poznać pełen zakres błędów, a nie tylko pierwszy napotkany. 

Warto wspomnieć również o testowaniu wyjątków, które często bywa pomijane. Tymczasem konstrukcja assertThrows pozwala nie tylko upewnić się, że wyjątek został rzucony, ale także że był to odpowiedni typ błędu. Podobnie jest z testowaniem kolekcji. Zarówno AssertJ, jaki i klasyczne asercje JUnit pozwalają wygodnie sprawdzać rozmiary, zawartość i właściwości list czy map. 

A co z metodami statycznymi? Choć ich testowanie bywa problematyczne, Mockito oferuje mechanizm mockStatic, który pozwala tymczasowo nadpisać zachowanie metody statycznej w bloku testu. Dzięki temu nawet kod, który nie został zaprojektowany do łatwego testowania, może być objęty kontrolą jednostkową. 

Co jeszcze warto wiedzieć?

Na koniec nie sposób pominąć tematu narzędzi. Choć same testy są pisane w kodzie, warto mierzyć ich pokrycie (np. za pomocą JaCoCo), a także integrować je z CI, by uruchamiały się automatycznie. Ale pokrycie kodu to nie wszystko, równie ważne jest to, co właściwie jest testowane. Testy powinny odzwierciedlać oczekiwane zachowania, nie linijki kodu. 

Testy jednostkowe nie są ozdobą projektu ani checklistą na koniec sprintu. Są integralną częścią procesu tworzenia oprogramowania, nie tylko dla programistów. Dla testerów to okazja do lepszego zrozumienia logiki aplikacji, dokładniejszego opisu wymagań i skuteczniejszego wychwytywania regresji. A dobrze napisany test to taki, który po pół roku nadal mówi jasno: „Jeśli ta funkcja ma działać poprawnie, to musi zwrócić dokładnie ten wynik i żadne zmiany nie mogą tego naruszyć”.

Źródła:
https://medium.com/javarevisited/java-unit-testing-practical-guide-and-cheat-sheet-with-real-world-examples-7aaee774cff6