Spring Boot Test – testowanie aplikacji w Spring Boocie

Spring Boot Test

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.

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.

How to run tests with IntelliJ
Running tests with IntelliJ IDE.

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";
}
}
view raw HelloService.java hosted with ❤ by GitHub

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/a2d420c3a2702226d200bffe6c9c9714

Pojawił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();
view raw verify.java hosted with ❤ by GitHub

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.

Materiały dodatkowe

1 myśl na “Spring Boot Test – testowanie aplikacji w Spring Boocie”

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *