Spring Boot Test to zestaw narzędzi ułatwiający nam testowanie aplikacji napisanych z użyciem Springa. Testy w aplikacji są bardzo ważnym elementem, który zaniedbany szybko odbije się na jakości kodu i ilości błędów, które się pojawią. Ilość pracy, którą trzeba później włożyć w naprawienie zaniedbanych testów, jest nieporównywalnie duża do czasu, który można zainwestować, tworząc je od razu. Testowanie jest ważne z kilku powodów:
- Łatwo możemy znaleźć błędy w naszym kodzie,
- Pomagają w utrzymaniu kodu – dają pewność, że nie zepsuliśmy czegoś przy wprowadzaniu zmian,
- Dokumentuje użycie – można zaglądnąć do kodu testów, aby dowiedzieć się, jak używać jakiejś biblioteki, oraz pozwalają na szybkie i łatwe sprawdzenie zachowania biblioteki/języka,
- Dokumentują założenia projektowe – w testach możemy wyrazić oczekiwane zachowanie aplikacji, bywa, że ktoś przeoczy jakieś założenia w projekcie, lub nie dopilnuje, aby wszystko działało, tak jak trzeba przy zmianach. Jeśli mamy dobre testy – one powinny to wyłapać.
Często inne osoby, które będą pracować z naszym kodem, mogą łatwo coś zepsuć. Testy po pierwsze dopilnują, aby tak się nie stało. A po drugie dają pewność osobie, która pracuje z kodem, że nic nie zepsuła. Dzięki temu może szybciej skończyć zmiany i wdrożyć nową funkcjonalność, zamiast marnować czas na sprawdzanie, czy wszystko działa, tak jak należy.
Wpis jest częścią serii wpisów na temat Spring Boot. Zapraszam do zapoznania się z pozostałymi wpisami na blogu!
- Spring Boot – szybkie tworzenie aplikacji web w Javie
- Spring Boot – Interakcja z bazą danych czyli Spring Data JPA
- Spring Boot – Widoki
- Użycie Spring Security w Spring Boot
- Spring Boot Web – Przekazywanie zmiennych do aplikacji przez URL czyli użycie @RequestParam i @PathVariable
- Spring Boot Test – testowanie aplikacji w Spring Boocie
- Wsparcie dla pola typu JSONB w PostgreSQL dla Spring Data JPA + Hibernate
- Wysyłanie plików na serwer przez Spring Boot
- Spring Boot – Spring Data JPA część II: Powiązania między tabelami
- Spring Mail + Spring Boot – łatwe wysyłanie maili z aplikacji w Javie
- Docker + Spring Boot – zamykamy aplikację w kontenerze Dockerowym
- Java Bean Validation + Spring Boot – sprawdzanie poprawności danych w Spring Boocie
- Spring Boot + Swagger UI
- Serwer Amazon EC2 i SSL od Let’s encrypt w aplikacji Spring Boot
Spring Boot Starter Test
Jak już wspomniałem wcześniej, Spring dostarcza zestaw narzędzi, które ułatwiają testowanie aplikacji w nim napisanych. Dzięki nim możemy łatwo uruchomić kontekst Springa, który zadba o wstrzyknięcie zależności tam, gdzie trzeba lub odpalić całą aplikację do testów integracyjnych. Ułatwia to bardzo życie i oszczędza czas poświęcony na przygotowanie testów. Dodając do zależności moduł starter test, otrzymujemy cały zestaw użytecznych bibliotek, wśród których możemy znaleźć:
- JUnit – Podstawowa biblioteka do uruchamiania testów,
- Mockito – biblioteka do tworzenia Mockowych obiektów,
- AssertJ – biblioteka zawierająca assercje,
- Spring Test i Spring Boot Tests – zestaw narzędzi do testowania Springa i Spring Boota,
Oprócz tego pakiet zawiera jeszcze inne biblioteki, jednak wykraczają one poza temat tego postu.
Wszystkie przykłady znajdują się jak zawsze w repozytorium na GitHubie, dostępnym pod adresem: https://github.com/mloza/spring-boot-test
Aplikacja testowa
Aby w pełni zaprezentować możliwości Springa w temacie testów, zacznijmy od stworzenia bardzo prostej aplikacji testowej. Na początek zacznijmy od stworzenia kontrolera, który będzie nam zwracał jakiś prosty tekst.
@Controller | |
public class SimpleController { | |
@RequestMapping("/hello") | |
@ResponseBody | |
public String hello() { | |
return "Hello!"; | |
} | |
} |
Po uruchomieniu aplikacji i wejściu na stronę http://localhost:8080/hello aplikacja zwróci nam tekst Hello!.
Pierwszy test
Dodajmy test, który sprawdzi nam automatycznie czy zwracany tekst jest zgodny z oczekiwaniami. Możemy to zrobić na dwa sposoby. Pierwszy z nich to wstrzyknięcie kontrolera do testu i wywołanie metody. W ten sposób sprawdzimy, czy Spring znalazł nasz kontroler i czy metoda wykonywana jest prawidłowo. Drugim sposobem jest wykorzystanie mockMvc i wykonanie zapytania. W ten sposób sprawdzimy dodatkowo, czy prawidłowo mapujemy adres oraz, czy serializacja danych działa tak jakbyśmy tego oczekiwali. W tym przypadku lepiej skorzystać z 2 sposobu, ponieważ sprawdzamy w ten sposób więcej aspektów. Wstrzykiwanie komponentów do testu pokażemy sobie przy testowaniu serwisów.
MockMVC
Kod testu z użyciem mockMvc wygląda następująco:
@RunWith(SpringRunner.class) #1 | |
@WebMvcTest #2 | |
public class SimpleControllerTest { | |
@Autowired | |
private MockMvc mockMvc; | |
@Test | |
public void shouldReturnExpectedText() throws Exception { | |
mockMvc | |
.perform(get("/hello")) #3 | |
.andExpect(status().isOk()) #4 | |
.andExpect(content().string(containsString("Hello!"))); #5 | |
} | |
} |
Testy możemy uruchomić bezpośrednio z IDE, jeśli używasz IntelliJ to w widoku kodu, obok nazwy klasy lub nazwy metody, po lewej stronie będzie mała, zielona strzałka. Kliknięcie na nią spowoduje pokazanie się menu, z którego możemy wybrać Run lub Debug Test. Jeśli wykonywaliśmy wcześniej test i z jakiegoś powodu się nie powiódł, strzałka będzie czerwona z wykrzyknikiem.
U mnie jest trochę więcej opcji uruchomienia, dzięki zainstalowanym dodatkom. Nie powinieneś się przejmować jeśli ich nie widzisz.
Drugim sposobem jest wywołanie mvn test z lini poleceń, wtedy testy zostaną uruchomione bezpośrednio z mavena. W ten sposób zostaną wykonane wszystkie testy w projekcie których klasy kończą się nazwą Test.
Nowe adnotacje
Mamy tutaj 2 nowe adnotacje, @RunWith (#1) oraz @WebMvcTest (#2). Pierwsza z nich, mówi silnikowi testów (którym w przykładzie jest JUnit), aby uruchomił ten test z użyciem SpringRunnera. Dzięki temu framework wie, że to będzie test z wykorzystaniem Springa i powinien wstrzyknąć zależności. Dodatkowo również szuka innych adnotacji takich jak @WebMvcTest i podejmuje odpowiednie działania.
Druga adnotacja (@WebMvcTest (#2)) mówi, że chcemy testować komponenty MVC, więc skonfiguruje dodatkowe elementy, które będą nam potrzebne, takie jak mockMvc. W tym przypadku zostanie skonfigurowany cały kontekst springowy, ale nie będzie tworzony serwer. Dzięki temu cały test uruchomi się szybciej i nie będzie potrzebował dodatkowych portów.
Asercje
Spójrzmy na kod samych testów. W punkcie oznaczonym jako #3 mówimy, że chcemy dostać to, co znajduje się pod adresem /hello. Następnie upewniamy się, że zapytanie zostało wykonane poprawnie i zwrócono nam kod 200 OK (#4). Ostatnim elementem jest sprawdzenie zawartości odpowiedzi, oczekujemy, że odpowiedź zawiera tekst Hello!
Więcej informacji (debugowanie)
Jeżeli test z jakiegoś powodu nie przechodzi, możemy dodać po punkcie #3 linię .andDo(print()), dzięki której zostaną wypisane na konsole wszystkie informacje o requeście. U mnie wygląda to następująco:
MockHttpServletRequest: HTTP Method = GET Request URI = /hello Parameters = {} Headers = {} Handler: Type = pl.mloza.controllers.SimpleController Method = public java.lang.String pl.mloza.controllers.SimpleController.hello() Async: Async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: Attributes = null MockHttpServletResponse: Status = 200 Error message = null Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[6]} Content type = text/plain;charset=UTF-8 Body = Hello! Forwarded URL = null Redirected URL = null Cookies = []
Bardzo dużo informacji diagnostycznych, które pomogą nam w znalezieniu przyczyny błędu. W tym miejscu zachęcam do sprawdzenia, co się stanie jeśli będziemy oczekiwać innego tekstu, niż zwróci nam kontroler lub innego statusu.
Serwis
Dodajmy do aplikacji serwis, który będzie zwracał dodatkowy tekst do odpowiedzi z kontrollera.
@Service | |
public class HelloService { | |
public String getHello() { | |
return "World"; | |
} | |
} |
Użyjmy go w kontrolerze, tak aby pod nowym endpointem zwracał nam tekst Hello World! Nasz kontroler w tym momencie wygląda następująco:
@Controller | |
public class SimpleController { | |
@Autowired | |
private HelloService helloService; | |
@RequestMapping("/hello") | |
@ResponseBody | |
public String hello() { | |
return "Hello!"; | |
} | |
@RequestMapping("/helloworld") | |
@ResponseBody | |
public String helloWorld() { | |
return "Hello " + helloService.getHello() + "!"; | |
} | |
} |
Jeśli uruchomimy naszą aplikację i wejdziemy w przeglądarce pod adres http://localhost:8080/helloworld, powinniśmy otrzymać spodziewany tekst: Hello World!.
Test kontrolera z serwisem
Jeśli w tym momencie spróbujesz uruchomić poprzedni test, skończy się on błędem mówiącym nam o tym, że nie może znaleźć Beana pasującego do pola HelloService. Dziej się tak dlatego, że adnotacja @WebMvcTest inicjalizuje tylko komponenty związane z MVC. Możemy ten problem rozwiązać na dwa sposoby, dostarczyć mockowy serwis lub zmienić adnotację. Zacznijmy od zmiany adnotacji oraz dodania testu do nowego serwisu. Zmodyfikujmy klasę testową, aby wyglądała następująco:
@RunWith(SpringRunner.class) | |
@SpringBootTest #1 | |
@AutoConfigureMockMvc #2 | |
public class SimpleControllerTest { | |
@Autowired | |
private MockMvc mockMvc; | |
@Test | |
public void shouldReturnExpectedText() throws Exception { | |
mockMvc | |
.perform(get("/hello")) | |
.andDo(print()) | |
.andExpect(status().isOk()) | |
.andExpect(content().string(containsString("Hello!"))); | |
} | |
@Test | |
public void shouldReturnExpectedTextFromService() throws Exception { | |
mockMvc | |
.perform(get("/helloworld")) | |
.andDo(print()) | |
.andExpect(status().isOk()) | |
.andExpect(content().string(containsString("Hello World!"))); | |
} | |
} |
Nowe adnotacje
Pojawiły się nowe adnotacje: @SpringBootTest (#1) oraz @AutoConfigureMockMvc. Pierwsza mówi nam, że chcemy testować cały kontekst springa, nie tylko MVC, a druga, że chcemy mieć nadal możliwość korzystania z MockMvc. Z tymi adnotacjami test powinien być już zielony.
Mockowanie serwisu
Drugim sposobem jest dostarczenie mocka serwisu. W naszym przypadku serwis jest bardzo prosty i nie było sensu go mockować. Jednak gdy serwis jest skomplikowany, odwołuje się do innych serwisów lub aplikacji, których nie chcemy uruchamiać, po to, tylko aby sprawdzić jakąś małą część kontrolera, możemy dostarczyć zaślepkę, czyli mocka. Pozwala on także, na wykonanie dodatkowych assercji. Zacznijmy od dodania mocka do testu. Nie musimy przywracać starych adnotacji, aby tego dokonać. Zmodyfikujmy naszą klasę testową do postaci:
https://gist.github.com/a2d420c3a2702226d200bffe6c9c9714Pojawiło nam się nowe pole (#1) z naszym HelloService, jednak adnotacja @MockBean powoduje, że zamiast prawdziwego serwisu wstawiony zostanie tam automatycznie mock. Dodatkowo wszędzie gdzie był użyty nasz serwis, zostanie on zastąpiony tym mockiem. W punkcie #2 konfigurujemy zachowanie mocka. Dosłownie mówimy, że gdy zostanie wywołana metoda getHello naszego serwisu, to zwróć Mock. W punkcie #3 sprawdzamy, czy rzeczywiście tak się stało i otrzymaliśmy tekst Hello Mock!.
Asercje na mockach
Wspominałem wcześniej, że mocki dostarczają nam dodatkowe metody weryfikacji. Możemy na przykład, sprawdzić ile razy została wywołana nasza metoda.
verify(service, times(1)).getHello(); |
Nasza metoda nie przyjmuje parametrów, jednak również możemy sprawdzić przy pomocy mocka czy została wywołana z prawidłowymi parametrami. O mockach postaram się przygotować oddzielny wpis, ponieważ jest to dość obszerny temat.
Test serwisu
Na koniec jeszcze jak sprawdzić sam serwis. Możemy wstrzyknąć go do metody testowej za pomocą adnotacji @Autowired i wykonać na nim asercje:
@RunWith(SpringRunner.class) | |
@SpringBootTest | |
public class HelloServiceTest { | |
@Autowired | |
private HelloService helloService; | |
@Test | |
public void shouldReturnCorrectText() { | |
assertThat(helloService.getHello()).isEqualTo("World"); | |
} | |
} |
W tym przykładzie widzimy użycie asercji z biblioteki AssertJ. Osobiście bardzo je lubię, ponieważ można tworzyć łańcuch asercji (np. asserThat(list).hasSize(2).containsExactlyInAnyOrder(„one”, „two”)), bardzo przejrzyście opisują, co sprawdzają oraz posiadają bardzo dużo gotowych sprawdzianów.
Podsumowanie
Jak widzicie testowanie z użyciem narzędzi dostarczonych przez Springa, jest bardzo proste i szybkie. Dzięki temu pisanie testów jest mniej męczące i może powstać ich więcej, co przełoży się na mniej błędów i wyższą jakość kodu. Może się wydawać, że testowanie nie jest ważnym elementem projektu, jednak przy większym refactoringu lub powrocie do kodu po czasie okaże się, że czas poświęcony na przygotowanie dobrych testów zwrócił się wielokrotnie. Głównie przez zmniejszenie czasu potrzebnego na szukanie i naprawianiu błędów.
Wpis jest częścią serii wpisów na temat Spring Boot. Zapraszam do zapoznania się z pozostałymi wpisami na blogu!
- Spring Boot – szybkie tworzenie aplikacji web w Javie
- Spring Boot – Interakcja z bazą danych czyli Spring Data JPA
- Spring Boot – Widoki
- Użycie Spring Security w Spring Boot
- Spring Boot Web – Przekazywanie zmiennych do aplikacji przez URL czyli użycie @RequestParam i @PathVariable
- Spring Boot Test – testowanie aplikacji w Spring Boocie
- Wsparcie dla pola typu JSONB w PostgreSQL dla Spring Data JPA + Hibernate
- Wysyłanie plików na serwer przez Spring Boot
- Spring Boot – Spring Data JPA część II: Powiązania między tabelami
- Spring Mail + Spring Boot – łatwe wysyłanie maili z aplikacji w Javie
- Docker + Spring Boot – zamykamy aplikację w kontenerze Dockerowym
- Java Bean Validation + Spring Boot – sprawdzanie poprawności danych w Spring Boocie
- Spring Boot + Swagger UI
- Serwer Amazon EC2 i SSL od Let’s encrypt w aplikacji Spring Boot
Materiały dodatkowe
- Spring Boot – szybkie tworzenie aplikacji web w Javie
- Dokumentacja Spring Boot Tests https://spring.io/guides/gs/testing-web/
Dobry tutorial, dziękuję!