
Czym byłaby jakakolwiek aplikacja bez testów dowodzących, że spełnia założone wymagania? W dzisiejszym artykule z cyklu „Czym jest…?” skupimy się na frameworkach z dziedziny QA: JUnit, Mockito i im podobnych.
Uwaga! W tej serii zajmuję się opisowym przedstawieniem tematu, a celem jest zapoznanie z zagadnieniem w sposób luźny i zrozumiały dla nowicjusza. W związku z tym wiele elementów siłą rzeczy musi zostać pominiętych. Tych z was, którzy chcieliby się dowiedzieć więcej, zapraszam do źródeł na końcu artykułu.
Co powinien robić program?
Ktoś powie:
Co za idiotyczne pytanie! Oczywiście, że to do czego został stworzony! Kalkulator ma kalkulować, odtwarzacz filmów – odtwarzać filmy, a klient email odbierać i wysyłać maile.
No właśnie, sęk w tym, że to nie do końca prawda. Każda, ale to każda aplikacja, ma dwa zadania:
- Spełniać swoją funkcję.
- Nie spełniać żadnej innej funkcji.
A co ciekawe, ten drugi punkt jest tak samo ważny (o ile nie ważniejszy!) jak ten pierwszy.
Wyobraźcie sobie, że pobieracie skądś kalkulator, który kalkuluje – i robi to doskonale! – a jednocześnie kopie sobie bitcoiny dla jakiegoś cwaniaka. Albo klient email, który pięknie odbiera i cudownie wysyła maile… nie tylko do ustawionego odbiorcy, ale również wszystkich innych zapisanych kontaktów.
Żeby uniknąć takich i podobnych sytuacji, powstało coś takiego jak QA – Quality Assurance (ang. zapewnienie jakości). QA to zazwyczaj osobny dział w firmie gdzie pracują sami testerzy – co jednak nie zwalnia programistów z obowiązku pisania softu pozbawionego błędów. Robert Martin (aka Wujek Bob) w swojej książce „The clean coder” wielokrotnie powtarzał, że mottem profesjonalnego deva powinno być:
QA must find nothing!
(ang. Nie zostawiaj niczego dla QA!)

Ok, ale JAK właściwie testować aplikacje?
Na początku naszej drogi w kierunku zawodu programisty, testujemy głównie manualnie: odpalamy kodzik i sprawdzamy wszystkie opcje. W przytoczonym powyżej prostym kalkulatorze robilibyśmy po kolei: dodawanie, odejmowanie, mnożenie i dzielenie. Jeśli wyniki będą zgodne z tym co nauczyła nas podstawówka, to znaczy, że appka działa. Proste.
Sytuacja komplikuje się wraz z projektowaniem bardziej rozbudowanych programów. Jak dołożymy tam pierwiastki, potęgi, silnie, sinusy, cosinusy, zapamiętywanie wyników, funkcje… to takie manualne przetestowanie nowego commita zajmie nam kupę czasu. A to w konsekwencji sprawi, że będziemy to robić rzadko, np. raz na kilka godzin pracy, albo kilka dni…
…albo wcale.
Nie tędy droga. O ile testy manualne są jak najbardziej potrzebne – w obecnych czasach robi się je już na samym końcu, tuż PRZED* oddaniem efektów pracy klientowi.
(*) Albo jak pokazuje branża gamedev – już PO oddaniu efektów pracy klientowi. Nazywają to „dostęp Early Access” i za taką przyjemność przetestowania na wpół-działającej gry trzeba zapłacić, nierzadko jak z pełnoprawny produkt.

Aby testowało się przyjemnie, szybko i często, powstały liczne biblioteki z klasami i metodami to umożliwiającymi, wśród nich właśnie tytułowe JUnit oraz Mockito. Zanim jednak do nich przejdziemy, odpowiedzmy jeszcze na pytanie:
Ale CO testować?
Oj, rodzajów testów jest sporo! Powstała nawet specjalna infografika w postaci piramidy, która pokazuje hierarchię ważności i kolejność wykonywania poszczególnych rodzajów testów:

Jak widać, są tam różne typy testów, od jednostkowych (Unit tests), które nie bez powodu są podstawą piramidy, aż po testy rynkowe (Sensing the world) na samym szczycie. Wraz z przeprowadzaniem kolejnych rodzajów testów maleje wiara w przypuszczenia (assumptions) a rośnie pewność, że całość działa, dobrze razem współgra i poradzi sobie jako gotowy produkt (confidence in integrations).
W tym miejscu pewnie część z was zastanawia się dlaczego te etapy ubrano w obrazek piramidy, a nie na przykład strzałki/osi czasu. Wiele serwisów ponadto pokazuje te zależności w formie odwróconej piramidy – chodzi o to by pokazać, że im wyżej, tym testy trwają dłużej, a ich koszty rosną. Najtańsze testy są na samym dole – to te jednostkowe o których dziś mówimy. Jeśli będziemy na nich oszczędzać, to z pewnością zabulimy więcej gdzieś na wyższych etapach. Cóż, chytry dwa razy traci.
Jak dokładnie wyglądają poszczególne etapy testów?
- testy jednostkowe (to są testy w których po prostu sprawdzamy czy dana metoda robi to co powinna, np. czy 2 + 2 to faktycznie jest 4)
- testy integracyjne (tutaj sprawdzamy czy poszczególne elementy programu poprawnie ze sobą współpracują, np. połączenie z bazą danych albo komunikacja serwisów z kontrolerami)
- testy akceptacyjne (te są często pisane albo przez samego klienta, albo na podstawie jego wymagań – moment w którym pozytywnie przechodzą wszystkie testy akceptacyjne jest uznawany za koniec rozwoju appki)
- testy end2end (testy często manualne, sprawdzające poprawność działania od strony końcowego użytkownika)
Chociaż jako doświadczeni programiści będzie mogli wtrącić swoje trzy grosze na każdym z powyższych etapów, dzisiaj skupimy się na testach jednostkowych (i po trochu integracyjnych) z którymi z pewnością będą mieć styczność także junior developerzy.
Testowanie krok po kroku
Najprostszym sposobem na otestowanie wybranej metody jest dodanie zależności JUnit w mavenowym pom.xml (patrz: tekst o Mavenie):
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.2</version>
//wersja biblioteki, do wyboru
<scope>test</scope>
</dependency>
Kiedy już to zrobimy, wchodzimy na wybraną klasę w IntelliJ i wciskamy kombinację klawiszy:
CTRL + SHIT + T
Tym sposobem możecie stworzyć w pakiecie /test* klasę, której zadaniem będzie sprawdzenie czy metody klasy na bazie której powstała działają poprawnie.
(*) Mówimy o pakiecie Mavenowym i to bardzo ważne, bo mimo, że znajduje się w innym katalogu, to Maven (a wraz z nim IDE) traktuje taką klasę testującą zupełnie jakby była w pakiecie z klasą testowaną. A dzięki tej sztuczce możemy śmiało testować metody o dostępie pakietowym (bez modyfikatora 'public’)

Skoro klasa testująca jest już postawiona to warto byłoby tam coś wrzucić. Zanim to jednak zrobimy, spójrzmy jeszcze na parę zasad standaryzujących to jak pisać testy.
Nazewnictwo
Metoda testująca powinna mieć wymowną nazwę, czasem nawet bardzo długą, ale to bez znaczenia – ważne aby od razu wiadomo co jest testowane. Dla przykładu:
void add_firstArgumentOk_And_noSecond_throwsIllegalArgumentException()
Powyższa metoda testuje czy jeśli podamy pierwszy argument dodawania (firstArgumentOk) ale nie podamy drugiego (noSecond) to faktycznie zostanie wyrzucony wyjątek ’IllegalArgumentException’.
Struktura
O ile komentarze w kodzie produkcyjnym nie są mile widziane, o tyle przyjęło się, że w testach oddzielamy poszczególne części metody takimi komentarzami jak:
- //given
- //system under test
- //when
- //then
Sekcja ’given’ przygotowuje środowisko testowe: tutaj tworzymy zmienne i nadajemy wartości.
’System under test’ nie występuje tak często jak pozostałe – raczej tylko przy testowaniu bardziej rozbudowanych metod, gdzie najpierw trzeba zbudować obiekt który będziemy testować z elementów nakreślonych w 'given’.
W części ’when’ następuje użycie testowanej metody, natomiast w ’then’ porównanie (asercja) jej wyników z tym co faktycznie miała zwrócić.
Asercje
Najprostszy test wygląda tak, że najpierw używamy metody z argumentami z sekcji 'given’, a następnie porównujemy otrzymany wynik z tym czego oczekujemy.
//given
int x = 2;
int y = 2;
//when
int sum = add(x,y);
//then
assertEquals(sum, 4);
W powyższym przykładzie metoda assertEquals() pochodząca z biblioteki JUnit sprawdza czy wartość zwracana przez metodę add(x,y) rzeczywiście równa się 2. Jeśli tak – test przeszedł i zaświeci się na zielono, w przeciwnym wypadku kolor czerwony oznaczający, że coś nie bangla.

Frameworki do testów
Póki co piszemy jedynie o bibliotekach JUnit – są najpopularniejsze i najłatwiejsze na start. Ale nie jedyne.
Świetnym uzupełnieniem JUnit jest Mockito. Słówko ’mock’ w języku angielskim oznacza 'przedrzeźniać’ albo 'podszywać się pod coś/kogoś’. I to jest mniej więcej funkcja Mockito, które głównie stara się ułatwić developerowi przygotowanie środowiska testowego (sekcja 'given’).
Często metoda, którą chcemy przetestować pobiera jakieś parametry. Jeszcze pal licho jak są tam jakieś Stringi albo typy proste. Gorzej jeśli są tam obiekty do stworzenia których potrzebujemy innych obiektów, do stworzenia których potrzebujemy innych obiektów, do stworzenia których potrzebujemy innych obiektów, do stworzenia których potrzebujemy innych obiektów…
…i tak dalej, i tak dalej.
W tym miejscu zamiast szukać dobrego miejsca na postawienie stołka i zawieszenie paska od spodni, programista może po prostu użyć metody Mockito i „podszyć się” pod wymagane obiekty:
NeededObject mockObject = mock(NeededObject.class);
I gotowe. W dodatkowych metodach Mockito możemy ponadto odpowiednio ustawić zachowanie tego zamockowanego obiektu, tak by jego atrybuty pasowały do naszych testów.
Na koniec
Jak z pewnością zauważyliście, dzisiejszy tekst już rozrósł się do granic możliwości, choć w założeniu miał jedynie na szybko opisać temat. Chciałem jeszcze dodać coś więcej o innych popularnych frameworkach, ale – aby nie przytłoczyć nawałem informacji – ograniczę się tylko do pojedynczych zdań:
- AssertJ jest uzupełnieniem JUnit, daje dostęp do bardziej precyzyjnych asercji
- TestNG to taki ulepszony JUnit pozwalajacy m.in. na grupowanie testów czy testowanie współbieżne
- Spock, podobnie jak TestNG, chce wygryźć JUnit ze stołka lidera – jego plusy to łatwiejsza składnia (ale piszemy go w języku Groovy, który nieco różni się od Javy), wbudowane metody mockujące oraz możliwość testowania metod prywatnych
- Selenium to już właściwie cała platforma skierowana nie tyle dla samych programistów co dla testerów QA, nastawiona na testy end2end

Uff. Na dzisiaj tyle i jak zwykle dziękuję za uwagę. W następnym odcinku odpowiemy na pytanie „Czym jest REST (i SOAP)?”.
Źródła:
Dlaczego wolę pisać testy w Spocku? YT/ANG
Seria wpisów o testowaniu w Javie (ANG)