JUnit 5 – Wstęp
JUnit 4 jest już stary. Został opublikowany gdzieś w okolicach 2006 roku. Ostatnia aktualizacja (4.12) to rok 2014. Sama biblioteka nie jest zła, jeśli chodzi o testy jednostkowe. Spełnia swoje zadanie i nie ma powodu by się do czegoś przyczepić. Przez ostatnie 11 lat zaszło trochę zmian w języku jak i w sposobach wytwarzania oprogramowania. Przyszedł już czas na aktualizację.
Rzeczy które w JUnit 4 mi nie pasują
JUnit jest świetny, jeśli chodzi o testy jednostkowe. I tylko jeśli chodzi o testy jednostkowe. Jakakolwiek próba użycia go w testach integracyjnych jest z góry skazana na niepowodzenie. JUnit został zaprojektowany tak, by testy w nim pisane były niezależne, szybkie, powtarzalne i automatyzowalne. Innymi słowy, by spełniały zasadę F.I.R.S.T. Piąty element tej zasady jest związany z pokryciem. Jednak zaczęliśmy używać JUnita do tworzenia testów integracyjnych i integracyjno-jednostkowych. W efekcie mechanizm związany z @RunWith stał się zaczepem dla pluginów. Coś, co miało pomagać w migracji z wersji 3.x, służy nam do zupełnie czegoś innego.
W dodatku mamy do czynienia z klasycznym dependency hell, gdy potrzebujemy wykorzystać kilka runnerów. Który będzie pierwszy? Czy kolejność jest prawidłowa? Co będzie, jak zamienię kolejnością runnery? Słabo.
Kolejna rzecz to zarządzanie zależnościami pomiędzy testami. JUnit tego nie ma. Czasami jednak jest to przydatna funkcjonalność. Nie tylko na poziomie testów integracyjnych, ale też po to by testy jednostkowe można było wykonywać według zasady „fail fast”. Co prawda można na poziomie np. mavena ustawić odpowiednią regułę, ale dotyczy ona wszystkich testów. Przydałaby się granulacja na poziomie klasy albo przynajmniej pojedynczego zestawu.
Paskudnie rozwiązane zarządzanie zależnościami. W sumie nie ma się co dziwić, bo prawdziwy test jednostkowy nie ma zależności. Tyle tylko, że czasami warto by jakąś fabryczkę do mocków zrobić, albo dorzucić jakiś niewielki helper… a tu dupa i trzeba to ręcznie wiązać, albo za pomocą runnerów.
Ostatnia rzecz to asercje. Pozostało po starszych wersjach i nie idzie tego zmienić. Z drugiej strony wystarczy podpiąć AssertJ i olać asercje JUnita.
Dlatego lubię TestNG
TestNG jest znacznie młodsze. Wersja 1.0 jest z 2004 roku, ale najstarsza na githubie wersja 5.13 z 2010. Najnowsza 6.11 z końca lutego 2017 🙂 Mamy tutaj wszystko to, co w JUnit jest upierdliwe, uciążliwe, albo czego nie ma, a by się przydało. Po pierwsze zależności pomiędzy testami możemy skonfigurować na poziomie samych testów. Zatem można przygotować zestaw testowy tak, by wywalenie się pojedynczego testu skutkowało nieuruchomieniem tylko części testów. To z kolei oznacza, że można tworzyć testy integracyjne we w miarę łatwy sposób. Co więcej, można budować całe drzewa testów. Wystarczy wykorzystać grupy, które można zagnieżdżać. Do tego dochodzi całkiem dobra granulacja Before/After i już mamy w pełni elastyczne narzędzie.
Po drugie TestNG posiada na pokładzie mechanizm modułów. Chcesz zrobić sobie takie małe DI, by postawić część kontekstu? Proszę bardzo, jeżeli tylko trzymasz się JSR-330, czyli Dependency Injection for Java. Jako implementacja wykorzystywany jest Google Guice, co daje całkiem wygodne API. Swoją drogą im częściej porównuję Springa i Guice tym bardziej lubię Guice.
Po trzecie may też do dyspozycji mechanizm DataProvider, który pozwala nam na szybkie tworzenie np. testów parametryzowanych. Coś, co w JUnit trzeba ogarnąć runnerem, tu działa out of box.
Rzeczy dobre w JUnit
Ciekawie rozwiązano problem weryfikacji wyrzucanych wyjątków. Możemy użyć do tego odpowiedniego parametru w adnotacji, ale to nie jest najlepsze podejście. Co prawda wychwycimy wyjątek, ale nie zawsze możemy powiedzieć, że to co złapaliśmy, jest tym, czego się spodziewamy. Dlaczego? Ponieważ czasami taki sam wyjątek może być wyrzucony w różnych punktach testu. Zamiast tak prymitywnego podejścia, dostępnego też w TestNG, możemy użyć adnotacji @Rule. Z jej pomocą zdefiniujemy odpowiednią regułę, która pozwoli nam na ciche przechwycenie wyjątku oraz jego weryfikację. TestNG posiada podobnie działający mechanizm.
Podejście JUnit5
Jak widać, jest trochę rzeczy złych w JUnit 4. Twórcy postanowili naprawić większość z nich. W tym celu całkowicie zmienili architekturę. Podzielili ją na trzy główne części.
Zanim przejdę do omówienia elementów, drobna uwaga. JUnit 5 jest obecnie w epoce milestonea łupanego. Opis dotyczy tego jak to wygląda na chwilę obecną. Xapewne nie będziemy mieli już wielu zmian, to jednak może okazać się, że gdy czytasz ten tekst jest on w jakimś stopniu nieaktualny.
JUnit Platform
Jest to główna „paczka” nowej biblioteki. Zawiera w sobie najważniejsze elementy. Po pierwsze Launcher, który jest odpowiedzialny za udostępnienie API platformy różnego rodzaju klientom. Dodatkowo wykonuje pracę związaną z wyszukaniem testów w classpath. Klientami są implementacje TestEngine. Sam TestEngine udostępniony przez platformę służy do filtrowania i uruchamiania testów w zależności od potrzeb. Chcesz mieć FitNess na pokładzie i uruchamiać testy pisane za pomocą tej biblioteki? Wystarczy dodać odpowiedni silniczek.
Same silniki testowe są wyszukiwane za pomocą mechanizmu SPI, czyli nie musimy nic dodatkowo konfigurować, by całość śmigała. Wystarczy, by nasza paczka spełniała założenia SPI. Kolejnym elementem jest commons, który zawiera bebechy i narzędzia. Używać na własną odpowiedzialność.
Do tego mamy jeszcze mały programik konsolowy do uruchamiania testów, pluginy do gradle i surefire oraz runner, pozwalający na uruchomienie testów JUnit 5 w środowisku JUnit 4. Ten ostatni element ma ułatwić migrację.
JUnit Jupiter
Jupiter to pierwszy z silników testowych dostarczonych przez bibliotekę. Służy do uruchamiania testów oraz ma API do tworzenia rozszerzeń. Rozszerzenia pozwalają na modyfikowanie środowiska testowego, dodawania pewnych elementów do testów itp. Pełnią podobną rolę co runnery JUnit4, ale robią to w zupełnie inny sposób.
JUnit Vintage
Drugi z silników testowych, służy do uruchamiania testów napisanych w JUnit 4 i Junit 3.
Konfiguracja
Konfiguracja JUnit 5 w mavenie jest śmiesznie prosta:
Listing 1. Konfiguracja w pom.xml
<build><plugins><plugin><artifactid>maven-surefire-plugin</artifactid><version>2.19</version><dependencies><dependency><groupid>org.junit.platform</groupid><artifactid>junit-platform-surefire-provider</artifactid><version>1.0.0-M3</version></dependency><dependency><groupid>org.junit.jupiter</groupid><artifactid>junit-jupiter-engine</artifactid><version>5.0.0-M3</version></dependency><dependency><groupid>org.junit.vintage</groupid><artifactid>junit-vintage-engine</artifactid><version>4.12.0-M3</version></dependency></dependencies></plugin></plugins></build><dependencies><dependency><groupid>org.junit.jupiter</groupid><artifactid>junit-jupiter-api</artifactid><version>5.0.0-M3</version><scope>test</scope></dependency><dependency><groupid>junit</groupid><artifactid>junit</artifactid><version>4.12</version><scope>test</scope></dependency><dependency><groupid>pl.pragmatists</groupid><artifactid>JUnitParams</artifactid><version>1.0.6</version><scope>test</scope></dependency></dependencies>
Przy czym jedna uwaga. Jak już chcemy korzystać z JUnit 5, to darujmy sobie próby konfiguracji JUnit 4 jako osobnego zadania dla surefire. Lepiej jest dodać silnik Vintage, który pod spodem po prostu odpala JUnit 4 niż kombinować z konfiguracją. Szkoda czasu, a i wynik będzie popieprzony. Dlaczego? Ponieważ JUnit 5 wykryje testy JUnit 4, ale bez silnika vintage ich nie uruchomi. Zatem w logu dostaniemy:
Listing 2. Uruchomienie testów JUnit 4 bez silnika Vintage
mar 28, 2017 11:38:09 AM org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines
INFO: Discovered TestEngines with IDs: [junit-jupiter]
Running pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4IgnoreTest
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec - in pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4IgnoreTest
Running pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4WithoutRunnersTest
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 sec - in pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4WithoutRunnersTest
Running pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4WithRunnersTest
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 sec - in pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4WithRunnersTest
Klasa jest, testu nie ma, a teraz pomyślcie, jak wygląda raport. Będzie zawierać wiele zer… to nie dobrze?
Podsumowanie
Czas na krótkie podsumowanie. JUnit 5 wygląda na bibliotekę, która pozwoli nam na lepsze zarządzanie testami. Same testy będą pisane trochę inaczej i istnieje szansa, że będą lepsze. Moim zdaniem warto już dziś przyjrzeć się temu projektowi i spróbować go wdrożyć we własnych projektach. Całość jest mniej więcej stabilna, a zatem nie będzie jakiś dziwnych wybuchów. W kolejnych artykułach przybliżę różnice pomiędzy JUnit 5 i 4, w zakresie tworzenia testów.