DSP17 / Uncategorized

Testowanie jednostkowe REST Controllerów Spring

Nie wiesz od czego zacząć – zacznij od testu!

Tak głosi to słynne TDD. Dobra, ale czasem nie wiadomo jak zacząć pisać test, mimo że implementacja jest dosyć prosta. I tu nasze podręcznikowe zasady zaczynają się kłócić z rzeczywistością.
Weźmy dzisiaj na stół operacyjny Controllery Springowe. Są to bloczki naszej aplikacji webowej, które przyjmują zapytania z zewnątrz w formie HTTP i tłumaczą je na wywołania odpowiednich metod. Zazwyczaj wszystko czym się zajmuje controller to przyjąć obiekt z requesta i oddelegować go do odpowiedniego service’u. Na pierwszy rzut oka tu nie ma co testować. No właśnie – na pierwszy rzut oka.

Controller to nie tylko wydmuszka

Pierwsze co należy zauważyć to fakt, że wywołanie controllera nie zachodzi tak samo jak wywołanie typowego servicu. W żadnym miejscu aplikacji nie wywołujemy jawnie metody controllera, tylko zostaje ona wywołana przez framework, kiedy serwer otrzymuje zapytanie z odpowiednim mięskiem w środku i tak też należy rozpatrywać testowanie jednostkowe controllerów – jako coś co przyjmuje zapytanie HTTP, a nie inaczej.
Kolejną sprawą jest to, że przed wywołaniem metody do której masz zmapowany dany URL, wywoływany jest także walidator obiektów przekazanych w zapytaniu. To także przydałoby się sprawdzić testami. Błędy mogą kryć się wszędzie.
Kiedy walidacja nie przechodzi, zazwyczaj wyrzucany jest wyjątek. Wyjątki mogą też pochodzić gdzieś z wewnatrz naszej aplikacji, a controller zazwyczaj ma zdefiniowane ExceptionHandlery. Może warto to przetestować?
Nie jestem zwolennikiem wszędobylskich DTO i konwerterów do nich (nie mówię też, że są całkowicie złe). Moim zdaniem nadużywanie ich zaśmieca aplikację i sprawia, że jest mniej czytelna. Zwłaszcza kiedy mamy odpowiednie narzędzia pozwalające na używanie tej samej klasy w warstwie usługowej i webowej bez obawy, że przykładowo gdzieś wypłynie nam hash hasła przez REST API. I tak, to też wypada przetestować.

Przykład na żywym mięsie

Weźmy sobie na stół operacyjny taki controller:

klasę User:

i napiszemy test sprawdzający czy w zwróconym JSONie widzimy passwordHash. Tyle i aż tyle.

Controller w separacji

Kiedy chcesz przetestować Controller w całkowitej separacji od reszty appki wcale nie musisz podnosić całego ciężkiego, springowego kontekstu webowego. Żeby uzyskać taki odseparowany controller użyjemy MockMVC w celu stworzenia zamockowanego kontenera MVC i Mockito, żeby zamockować nasze zależności w controllerze.

Mając tak napisaną klasę, możemy przystąpić do napisania naszego pierwszego testu

Test podzieliłem na standardowe 3 segmenty:
– given – tutaj ustawiamy stan klasy przed testowaną operacją
– when – tutaj uruchamiamy testowaną operację
– then – tutaj sprawdzamy poprawność zwróconego wyniku i czy otrzymaliśmy oczekiwany stan testowanej klasy.

Oczywiście jak będziesz pisał testy w swoim projekcie to komentarze given-when-then można sobie darować, ja je tutaj wrzuciłem tylko dla pokreślenia podziału testu. Kolejną rzeczą jaką warto pamiętać, to jest to, że w jednym teście (jednej metodzie z adnotacją @Test) testujemy tylko jedną rzecz! Innymi słowy – po asercjach (tutaj: seria wywołań .andExpect(…)) nie wywołujemy już kolejnych testowanych operacji. To tak zwany antypattern given-when-then-when-then…

Ale co tu się dzieje? Jedziemy pokolei!

Given

Mówimy Mockito, żeby na wywołanie userService.getCurrentUser() wewnątrz Controllera zwrócił nam dummyUsera (to przeze mnie napisana metoda zwracająca w pełni uzupełnionego Usera).

When

Wywołujemy URL „/players/current” na MockMVC w który opakowany jest nasz testowany PlayerController. Wrzuciliśmy go tam w metodzie setUp wywoływanej przed każdym testem.

Then

mockMvc.perform(…) zwraca obiekt typu

na którym możemy wykonywać asercje w postaci andExpect(…) (podobne trochę do tych znanych z JSowego Jasmine). Za ich pomocą można sprawdzić status odpowiedzi HTTP zwróconej z controllera, nagłówek z Content-Type, jak i samo body odpowiedzi. Co ważne w tym przypadku – sprawdzamy czy w odpowiedzi jest nieobecny hash hasła.

String przekazany do jsonPath(…) może być odpowiednio bardziej złożony. Po więcej szczegółów odsyłam do dokumentacji.

Leave a Reply

Your email address will not be published. Required fields are marked *