i Platformy Technologiczne Laboratorium nr 5 Java: testy jednostkowe z biblioteką JUnit Projekt opracowany w ramach laboratorium nr 5 będzie wykorzystywany w czasie laboratorium nr 6 należy zachować przygotowaną aplikację. W ramach zadania należy opracować testy jednostkowe dla aplikacji przygotowanej w czasie laboratorium nr 4. Głównym obiektem testów powinny być klasy biznesowe aplikacji (jeśli nie zostały wydzielone w czasie realizacji poprzedniego zadania, należy je teraz zdefiniować). W szczególności przetestować należy metody biznesowe związane z przetwarzaniem zamówień i zweryfikować poprawną obsługę sytuacji wyjątkowych, np. błędy walidacji, niedostępność produktów. Punktacja 0-5 pkt w zależności od kompletności testowanych scenariuszy wykonania. Aby uzyskać maksimum punktów należy uwzględnić przynajmniej następujące elementy: przetestować poprawne przebiegi nietrywialnych metod biznesowych (poprawne dane wejściowe skutkujące poprawnym rezultatem działania), przetestować obsługę sytuacji wyjątkowych (np. jeśli produkt nie jest dostępny w czasie składania zamówienia, następuje zgłoszenie wyjątku; jeśli walidacja się nie powiodła, testowana metoda sygnalizuje to zgodnie z przyjętym kontraktem), wykorzystać obiekt-zaślepkę (mock), który zastąpi instancję klasy EntityManager wykorzystywaną przez testowane komponenty biznesowe. Testy jednostkowe Implementując testy należy pamiętać o następujących zasadach: 1. Każdy test powinien skupiać się na jednym aspekcie działania testowanego komponentu. W przypadku niepowodzenia testu powinno być jasne jaka była tego przyczyna. Jeśli test weryfikuje wiele różnych aspektów działania komponentu, to nie wiadomo, który z nich zawiódł. 2. Testy powinny być proste. Należy unikać instrukcji sterujących typu if/for/while w metodach testowych. Aby test był przydatny, musi być poprawny. Łatwiej jest zapewnić poprawność prostego liniowego kodu niż kodu z rozgałęzieniami i wieloma poziomami zagnieżdżenia. Jeśli metoda testowa sama kwalifikuje się do testowania jednostkowego (meta-testowania?), to znaczy, że jest zbyt skomplikowana. Gdy test wymaga debugowania w celu określenia jego przebiegu, to nie jest dobrym testem jednostkowym. 3. Wzorzec Arrange-Act-Assert ułatwia organizację testu dzieląc go na trzy sekcje: Arrange przygotowanie danych wejściowych i wszystkich wymaganych obiektów, Act wykonanie operacji na testowanym obiekcie (zazwyczaj wywołanie pojedynczej metody), Assert weryfikacja czy uzyskane rezultaty są zgodne z oczekiwaniami. W. Korłub 1
4. Nazwy metod testowych nie muszą być zwięzłe. Metody testowe nie są wywoływane przez dewelopera w kodzie aplikacji są wywoływane przez automatyczne narzędzia, którym nie przeszkadzają długie nazwy. Nazwa metody testowej powinna jednoznacznie wskazywać jaki scenariusz wykonania jest testowany oraz jaki jest spodziewany rezultat, przykładowo: whenorderedbooknotavailable_placeorderthrowsoutofstockex(). Dzięki temu na podstawie raportu z wykonania testów można określić błędne zachowania testowanego obiektu w oparciu o same nazwy metod bez konieczności analizowania ich zawartości. 5. Zewnętrzne zależności testowanego obiektu należy zastąpić mockami (obiektamizaślepkami). Idea testowania jednostkowego polega na weryfikacji działania komponentu aplikacji w izolacji od jego zależności. Dzięki temu wiadomo, że gdy test się nie powiedzie, problem leży w testowanej klasie, a nie w innych komponentach, na które nie ma ona wpływu. JUnit oraz Mockito W aplikacjach opartych o Spring Boot biblioteki JUnit oraz Mockito są dołączane do zbioru zależności projektu za pośrednictwem zależności spring-boot-starter-test, która standardowo definiowana jest w pliku pom.xml. Klasy testowe należy umieszczać w podkatalogu src/test/java, a metody testowe należy opatrzyć adnotacją @Test. W przypadku projektu budowanego przy użyciu narzędzia Maven testy zostaną uruchomione, gdy nastąpi wywołanie fazy test. Przykładowo w wierszu poleceń (w głównym katalogu projektu): $ mvn test Ponadto środowiska programistyczne typowo umożliwiają uruchamianie testów bez opuszczania IDE. W środowisku Netbeans można to uzyskać wybierając pozycję Test Project z menu Run. Jeśli wykorzystano przykładowy projekt, który w pliku pom.xml w sekcji <properties> zawiera flagę <surefire.tests.skip>true</surefire.tests.skip>, należy zmienić wartość tej flagi na false. Powyższe ustawienie w połączeniu z konfiguracją plug-ina maven-surefire-plugin (w dalszej części pliku pom.xml) umożliwia określenie czy testy mają być automatycznie uruchamiane przy każdym budowaniu projektu przez Mavena. W sekcji Assert testu przydatne są statyczne metody klasy narzędziowej org.junit.assert, które umożliwiają weryfikację podstawowych warunków, np.: assertequals wartość zwrócona przez testowaną metodę jest równa wartości oczekiwanej, assertarrayequals zwrócona tablica jest równa tablicy oczekiwanej, assertnotsame wartość zwrócona jest inna niż wartość podana w wywołaniu, assertfalse/asserttrue wartość zwrócona to false bądź true, assertnotnull wartość zwrócona jest inna niż null. Ponadto metoda assertthat umożliwia konstruowanie złożonych warunków z wykorzystaniem obiektów klasy Matcher. Najczęściej wykorzystywane obiekty klasy Mather można uzyskać posługując się statycznymi metodami klasy narzędziowej org.hamcrest.corematchers. Jeśli spodziewanym rezultatem wywołania metody na testowanym obiekcie jest wyrzucenie wyjątku, typ oczekiwanego wyjątku można zdefiniować w atrybucie expected adnotacji @Test. Tak W. Korłub 2
oznaczony test zakończy się sukcesem tylko wtedy, gdy zadeklarowany wyjątek zostanie faktycznie wyrzucony. Klasy testowe, w których wykorzystywana jest biblioteka Mockito, należy dodatkowo opatrzyć adnotacją @RunWith(MockitoJUnitRunner.class). Uzyskanie mocka jest możliwe na dwa sposoby: poprzez dodanie do klasy testowej pola opatrzonego adnotacją @Mock obiekt-zaślepka zostanie wstrzyknięty bezpośrednio do pola przez bibliotekę Mockito, ten wariant jest przydatny dla mocków wykorzystywanych we wszystkich metodach testowych danej klasy, przykładowo: @RunWith(MockitoJUnitRunner.class) public class OrdersServiceTest { @Mock EntityManager em; @Test(expected = OutOfStockException.class) public void whenorderedbooknotavailable_placeorderthrowsoutofstockex() { //... //przekazanie mocka do testowanego obiektu OrdersService ordersservice = new OrdersService(em } } //... programistycznie w kodzie metody testowej dla mocków wykorzystywanych tylko w obrębie jednego testu, przykładowo: EntityManager em = Mockito.mock(EntityManager.class //dalej operacje z użyciem mocka... Po uzyskaniu mocka należy skonfigurować jego zachowanie w sekcji Arrange testu. Służą do tego metody Mockito.when( ). W części when należy wywołać metodę mockowanego obiektu, której wynik ma zostać zdefiniowany. Z kolei w części thenreturn należy określić wartość zwracaną, gdy rozpatrywana metoda zostanie wywołana w czasie testu. Przykładowo, jeśli oczekiwane jest, że metoda find wywołana na mocku klasy EntityManager zwróci zdefiniowaną lokalnie instancję klasy encyjnej Book, należy zastosować następujące wywołanie w sekcji Arrange testu: Book book = new Book( book.setamount(0 Mockito.when( em.find(book.class, book.getid()) //mockowane wywołanie book //zwracana wartość W. Korłub 3
W przypadku, gdy wartości argumentów mockowanego wywołania nie są istotne, można zastąpić je wywołaniami metod any*() z klasy narzędziowej Matchers. Przykładowo, jeśli metoda find ma zwracać ten sam obiekt niezależnie od tego, jaki klucz główny został przekazany (drugi argument metody EntityManager.find()), zaślepkę można skonfigurować następująco: Mockito.when( em.find(eq(book.class), any(uuid.class)) book dowolny argument klasy UUID Z kolei w sekcji Assert testu można zweryfikować czy mock został wykorzystany w sposób zgodny z oczekiwaniami. Służy do tego statyczna metoda verify klasy Mockito. Poniższy kod pozwala sprawdzić, czy metoda persist obiektu-zaślepki em została wywołana dokładnie jeden raz, a argumentem wywołania był obiekt order: //Arrange Order order = new Order( EntityManager em = Mockito.mock(EntityManager.class //... Act test zapisu do bazy danych... //Assert Mockito.verify(em, times(1)).persist(order Pierwszy argument metody verify to obiekt-zaślepka, którego wykorzystanie będzie weryfikowane. Drugi argument określa spodziewane zachowanie, np.: times(x) w czasie testu metoda została wywołana dokładnie x razy, never() metoda nie została wywołana w czasie testu (np. jeśli zamówienie nie było poprawne, metoda persist nie została wywołana ani razu), atleast(x) metoda została wywołana przynajmniej x razy, atmost(x) metoda została wywołana nie więcej niż x razy. Jeśli w czasie testu liczba wywołań wybranej metody mocka była inna niż określono w metodzie verify, test zakończy się niepowodzeniem. Biblioteka Mockito umożliwia również budowanie zaślepek dla całych łańcuchów wywołań metod. Jest to szczególnie przydatne przy mockowaniu obiektów typu builder. Należy w tym celu wykorzystać wariant RETURNS_DEEP_STUBS w czasie konstruowania mocka. Przykładowo, aby uzyskać funkcjonalną zaślepkę dla obiektu-buildera klasy UriComponentsBuilder, można wykorzystać następujący kod: UriComponentsBuilder uribuilder = Mockito.mock( UriComponentsBuilder.class, //klasa do zamockowania RETURNS_DEEP_STUBS //tryb mockowania W. Korłub 4
Mockito.when( uribuilder //łańcuch wywołań metod na mocku:.path(any()).buildandexpand(book.getid()).touri() new URI("/books/17") W. Korłub 5