JUnit 5 to framework nowej generacji do tworzenia automatycznych testów jednostkowych w technologii Java, oferujący wiele ciekawych funkcji, takich jak np. testy parametryzowane, wstrzykiwanie zależności, całkowite wsparcie dla Javy 8, czy w końcu nowe API, umożliwiające łatwiejsze niż kiedykolwiek rozszerzanie możliwości frameworka i dostosowanie go do potrzeb projektowych.
Frameworki takie jak JUnit 5 wykorzystywane są dzisiaj nie tylko w testach jednostkowych. Mają również szerokie zastosowanie w testach wyższego poziomu, w tym w testach funkcjonalnych z użyciem takich bibliotek jak Selenium WebDriver czy REST Assured.
W tym wpisie przedstawię najważniejsze cechy JUnit 5, dzięki czemu nauczysz się samodzielnie przygotować projekt w oparciu o ten framework.
Dokumentacja JUnit 5
Dokumentacja JUnit 5 jest wyjątkowo dobrze napisana. Nie tylko zawiera szczegółową dokumentację frameworka, ale również wiele przykładów, które możesz wykorzystać tworząc swój projekt. To strona, która definitywnie powinna znaleźć się w Twoich ulubionych, jeśli zamierzasz uczyć się, a później korzystać z JUnit 5:
http://junit.org/junit5/docs/current/user-guide/
Przygotowanie środowiska
Do pracy z projektem opartym o JUnit 5 koniecznie jest środowisko z zainstalowanym Java Development Kit w wersji co najmniej 8 (JDK 8), zintegrowane środowisko programistyczne (Intellij IDEA Community lub Ultimate) oraz opcjonalnie system kontroli wersji Git.
Tip: Jeżeli pracujesz w systemie Windows 10, zastanów się nad użyciem menadżera pakietów takiego jak Chocolatey, który umożliwia zarządzanie wszystkimi aspektami związanymi z oprogramowaniem: instalacją, konfiguracją, aktualizacją i odinstalowaniem. Wystarczy kilka komend, aby przygotować środowisko z aktualnymi wersjami oprogramowania.
Jeśli chcesz, możesz skorzystać z opisu konfiguracji środowiska w systemie Windows, opracowanych na potrzeby projektu automatyzacji testów aplikacji internetowej z wykorzystaniem Selenium. Wiele spośród zawartych tam porad można zastosować również w opisywanym tutaj podstawowym projekcie:
- Narzędzia - system kontroli wersji Git i emulator konsoli Cmder
- Narzędzia - Selenium i przeglądarki internetowe
- Narzędzia - Java, Gradle i IntelliJ
Jeśli używasz systemu macOS, możesz skorzystać ze wskazówek dotyczących przygotowania środowiska zebranych w tym artykule:
Konfiguracja projektu
JUnit 5 składa się z kilku artefaktów zgrupowanych w JUnit Platform, JUnit Juipter i JUnit Vintage - w sumie z kilku bibliotek (architektura modularna), które wymagane są do prawidłowej kompilacji i uruchamiania projektów.
Pobierz junit5-gradle-template z repozytorium
Aby uprościć proces tworzenia projektu, przygotowałem gotowy do ściągnięcia szkielet aplikacji. Zaletą tego podejścia jest brak konieczności instalacji Gradle. Projekt zawiera tzw. Gradle Wrapper, który lokalnie zainstaluje zależności Gradle wymagane do jego uruchomienia.
git clone
Jeżeli masz zainstalowany system kontroli wersji Git, wystarczy sklonować repozytorium:
git clone https://gitlab.com/qalabs/blog/junit5-gradle-template.git
Tip: Dla systemu Windows istnieje sporo interesujących emulatorów konsoli, które ułatwiają codzienną pracę z wierszem poleceń. Warto zainteresować się takimi narzędziami, jak cmder lub Babun.
Nie masz Gita?
Pobierz ZIP ze strony projektu: https://gitlab.com/qalabs/blog/junit5-gradle-template i rozpakuj go na swoim komputerze.
Pierwsze budowanie projektu
Używając wiesza poleceń, przejdź do katalogu projektu (junit5-gradle-template) i wykonaj polecenie:
gradlew clean test
Komenda skompiluje kod projektu i uruchomi testy z użyciem Gradle. Jeśli wszystko zostało poprawnie skonfigurowane, powinieneś zobaczyć podsumowanie podobne do tego jak poniżej:
1 | Starting a Gradle Daemon (subsequent builds will be faster) |
Gdy utworzysz więcej testów, możesz używać powyższej komendy do wykonywania ich wsadowo (czyli właśnie z użyciem wiersza poleceń).
Import projektu do IntelliJ
Aby zaimportować projekt w IntelliJ, wybierz opcję File | Open i wskaż plik gradle.build. Otwórz plik jako projekt i poczekaj chwilę, aż zaimportuje się wraz ze wszystkimi zależnościami.
Wprowadzenie do JUnit 5
Zależności
Konfiguracja zależności znajduje się w pliku build.gradle. Najważniejsze z nich to plugin JUnit Platform dla Gradle, pozwalający uruchamiać testy, oraz Jupiter Engine i Jupiter API.
Tip: Przeczytaj więcej o definiowaniu zależności w
Gradle: https://docs.gradle.org/4.5.1/userguide/artifact_dependencies_tutorial.html
Podstawowe adnotacje
Podstawowe adnotacje używane w testach pochodzą z pakietu org.junit.jupiter.api i są to:
@BeforeAll- metoda oznaczona tą adnotacją będzie wykonana przed wszystkimi innymi metodami w klasie@BeforeEach- metoda oznaczona tą adnotacją będzie wykonana przed każdym kolejnym testem@Test- właściwa metoda testowa@AfterEach- metoda oznaczona tą adnotacją będzie wykonana po każdym kolejnym teście@AfterAll- metoda oznaczona tą adnotacją będzie wykonana po wszystkich innych metodach w klasie
Inne przydatne adnotacje:
@DisplayName- pozwala na dostosowanie wyświetlanej nazwy testu@Disabled- wyłącza test@RepeatedTest- wykonuje test konfigurowalną liczbę powtórzeń@Tag- pozwala na tagowanie testów
Podstawowy przykład:
1 | import org.junit.jupiter.api.*; |
Test możesz uruchomić bezpośrednio z IntelliJ lub możesz użyć podanej wcześniej komendy gradlew clean test.
Cykl życia instancji testu (@TestInstance)
W JUnit 5 domyślnie dla każdej metody testowej w klasie testowej zostaje utworzony nowy obiekt tej klasy. To tak zwany cykl życia per metoda (w nomenklaturze JUnit 5 to Lifecycle.PER_METHOD). Nie mniej, cykl życia instancji testu może zostać zmieniony na cykl życia per klasa (Lifecycle.PER_CLASS) za pomocą adnotacji @TestIntance. W trybie PER_CLASS pojedyncza instancja testu jest tworzona dla każdej metody testowej, a metody oznaczone adnotacjami@BeforeAll i @After nie muszą być statyczne.
Daje to bardzo ciekawe możliwości, szczególnie w testach, gdzie każda metoda testowa musi mieć dostęp do tej samej instancji danego obiektu. Takim obiektem może być np. instancja org.openqa.selenium.WebDriver w Selenium, gdzie raz zainicjowany sterownik do wybranej przeglądarki powinien być wykorzystany przez wszystkie testy w klasie testowej.
Przykład:
1 | import org.junit.jupiter.api.*; |
Konfiguracja cyklu życia może być również zmieniona globalnie dla wszystkich testów. Jednym ze sposobów jest użycie konfiguracji z pliku junit-platform.properties:
junit.jupiter.testinstance.lifecycle.default = per_class
Wstrzykiwanie parametrów do testów
W JUnit 5 metody testowe oraz inne metody klasy testowej mogą przyjmować parametry typu org.junit.jupiter.api.TestInfo, org.junit.jupiter.api.RepetitionInfo czy org.junit.jupiter.api.TestReporter. Dodatkowo, dzięki bardzo prostemu Extension API, definiowanie własnych typów parametrów w JUnit 5 staje się bardzo proste.
Zwróć uwagę na metody przyjmujące parametry. Te parametry są wstrzykiwane automatycznie podczas uruchomienia testu i dają programiście dodatkowe metody, z których może korzystać:
1 | class JUnit5BuiltInParameterResolution { |
Poza obiektem typu TestReporter, który daje możliwość publikowania wpisów do raportu z testów, pozostałe obiekty wbudowane (TestInfo, RepetitionInfo) mają raczej niewielkie zastosowanie w praktyce.
Nie mniej, jeżeli rozszerzymy JUnit 5 i pozwolimy mu na wstrzykiwanie własnych zależności, zastosowań praktycznych będzie znacznie więcej. Możliwe zastosowania to na przykład wstrzykiwanie obiektów konfiguracyjnych w testach oraz wstrzykiwanie obiektów dostarczających dane testowe.
Info: Rozszerzanie
JUnit 5z użyciemExtension APIjest poza zakresem tego artykułu. Jeżeli jesteś zainteresowany, to znajdziesz informacje na ten temat w dokumentacjiJUnit 5.
Asercje
Asercje w testach służą do weryfikacji zachowania lub stanu testowanego systemu (SUT - System Under Test). Niepowodzenie asercji kończy się w JUnit 5 wyjątkiem, który przerywa wykonanie aktualnego testu.
JUnit 5 dostarcza wiele wbudowanych asercji, których należy szukać w klasie org.junit.jupiter.api.Assertions.
Tip: Oprócz wbudowanych asercji, możesz też korzystać z bibliotek zewnętrznych. Polecam szczególnie
AssertJ. Więcej o integracjiAssertJiJUnit 5możesz przeczytać tutaj: JUnit meets AssertJ
Asercje podstawowe
Do podstawowych asercji należą: assertEquals, assertArrayEquals, assertSame, assertNotSame, assertTrue, assertFalse, assertNull, assertNotNull, assertLinesMatch, assertIterablesMatch
Przykład:
1 | import static org.junit.jupiter.api.Assertions.assertNotNull; |
Assert all
Assertions.assertAll weryfikuje, czy żadna z grupy asercji nie kończy się wyjątkiem:
1 |
|
Zwróć uwagę na wykorzystanie wyrażeń Lambda w przykładzie. Wyrażenia Lambda pozwalają na tworzenie instancji klas anonimowych, implementujących interfejs z jedną metodą w bardzo zwięzły sposób. Pozwalają one na traktowanie funkcjonalności jako argumentu do metody.
W powyższym przykładzie do metody assertAll przekazywane są dwa argumenty, które są anonimowymi implementacjami interfejsu org.junit.jupiter.api.function.Executable, wyrażonymi za pomocą wyrażenia Lambda.
Tip: Przeczytaj więcej o wyrażeniach
Lamdatutaj: https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
Zasada działania assertAll jest dość prosta: metoda weryfikuje, czy żadna z przekazanych do tej metody asercji nie zakończy się błędem. Co istotne, w przypadku assertAll wszystkie asercje przekazane jako argumenty zostaną wykonane, nawet jeśli jedna z pierwszych da wynik negatywny, a jeśli choć jedna zakończy się wyjątkiem, to cały test zakończy się błędem.
Dla powyższego przykładu zaraportowany zostanie następujący błąd:
1 | org.opentest4j.MultipleFailuresError: Multiple Failures (2 failures) |
Funkcjonalność ta może mieć szerokie zastosowanie w testach z wykorzystaniem Selenium, gdzie zatrzymanie testu w wyniku pierwszego napotkanego błędu może spowodować, że inne problemy nie zostaną wykryte, a ponowne uruchomienie testów w celu weryfikacji pozostałych asercji może zająć dużo czasu.
Podsumowanie
JUnit 5 oferuje jeszcze wiele innych funkcji, takich jak choćby weryfikacja czasu wykonania metod, testy parametryzowane, czy znakomite Extension API.