CodeGym /Kursy /Spring REST & MVC /@WebMvcTest: testowy wycinek warstwy web

@WebMvcTest: testowy wycinek warstwy web

Spring REST & MVC
Poziom 30, Lekcja 0
Dostępny

1. Skupienie na testowaniu warstwy web

Jeśli być szczerym, większość API „łamie się" nie dlatego, że ktoś celowo chciał wszystko zepsuć. Zazwyczaj dzieje się to przez małą zmianę „na pięć minut": zmienili nazwę pola w DTO, trochę przerobili kod statusu, dodali @Valid — i nagle klient przestał rozumieć twój serwer. Dlatego nasze dzisiejsze zadanie — nauczyć się testować właśnie zewnętrzny kontrakt kontrolerów, a nie zdawać egzamin z całej aplikacji na raz.

Na poziomie odczuć wydaje się: „No przecież sprawdziłem w Postmanie, wszystko działa". Problem polega na tym, że Postman — to świetna lupa do ręcznej weryfikacji, ale bardzo zły system alarmowy. Nie obudzi się w nocy i nie powie: „Hej, przypadkowo zacząłeś zwracać 200 zamiast 404". Autotesty są właśnie o tym: nie zastępują myślenia, ale czynią regresje zauważalnymi od razu, gdy jeszcze pamiętasz, co zmieniałeś.

Kontrakt jest już widoczny z zewnątrz: URI, DTO, walidacja, ProblemDetail, scenariusze multipart i dokumentacja są zebrane w jedną zrozumiałą powierzchnię API. Teraz ważne, żeby przypadkowa zmiana nie zmieniała tej umowy po cichu. Dlatego dalej wszystko buduje się po prostym łańcuchu: najpierw wybieramy rozsądny sufit testowy, potem zbieramy controller slice, utrwalamy happy-path, następnie negative-path i na końcu dochodzimy do endpointów plikowych i ogólnego audytu warstwy web.

I tu pojawia się ważna dyscyplina: co dokładnie chcemy utrwalić testami w kursie REST. W tym kursie budowaliśmy serwis bez bazy danych i security i to jest świadome ograniczenie. Dlatego dla nas „główny produkt" — to nie persistence, nie transakcje i nie infrastruktura, a warstwa kontraktu web/API: routing, binding, kształt JSON odpowiedzi, walidacja, globalna obsługa błędów, prawidłowe statusy i nagłówki. Właśnie ta warstwa powinna mieć swój „sufit testowania" — i w ramach kursu tym sufitem będzie web-layer slice przez @WebMvcTest.

Żeby brzmiało zupełnie po ludzku, można wyobrazić sobie API jak drzwi wejściowe do mieszkania. Nie potrzebujesz za każdym razem, sprawdzając zamek, rozkładać przy tym całej instalacji wodnej i przestawiać okablowania. Ale musisz być pewny, że drzwi otwierają się właściwym kluczem i że domofon nie odpowiada „200 OK" na każde naciśnięcie przycisku.

2. Kontrakt web w Task Tracker API

Gdy mówimy „warstwa web" w tym kursie, mamy na myśli nie abstrakcyjne „kontrolery". Mamy na myśli publiczny kontrakt, który widzi zewnętrzny klient: aplikacja mobilna, frontend, integracja, choćby skrypt w Pythonie. Klientowi jest obojętne, jak wewnętrznie zorganizowany jest twój serwis, ale absolutnie nie jest obojętne, co wróci na GET /api/v1/tasks i co się stanie z nieprawidłowym JSON.

Ten kontrakt ma kilka wymiarów i doskonale testuje się właśnie na poziomie Spring MVC:

Część kontraktu Przykład w projekcie Dlaczego to krytyczne Co faktycznie sprawdza test
Trasa + metoda HTTP GET /api/v1/tasks/{taskId} Klient musi trafić „do właściwej metody" status, czasem handler pośrednio przez wynik
Bindowanie danych wejściowych taskId ze ścieżki, page/size/sort z query Błąd często pojawia się „przed serwisem" że przy nieprawidłowym parametrze wraca prawidłowy błąd
Kody statusów 201 Created, 204 No Content, 404 Not Found, 409 Conflict To semantyka, a nie kosmetyka status().isCreated(), isNoContent() itd.
Nagłówki Location przy tworzeniu, Content-Disposition przy pobieraniu Kod klienta często jest od nich zależny header().string(...)
Kształt JSON PagedResponse<T>, detail DTO, payload błędu Stabilność kontraktu sprawdzenia jsonPath(...) formy
Walidacja @Valid dla create/update, ograniczenia dla criteria Nieprawidłowe wejście nie powinno trafiać do logiki 400 + code=INVALID_INPUT + błędy pól
Kontrakt błędów ProblemDetail + aplikacyjny code Błędy są częścią API application/problem+json i pola odpowiedzi

Zwróć uwagę na ważny niuans: prawie wszystkie te rzeczy sprawdzane są bez prawdziwego uruchamiania serwera na porcie, bez prawdziwej bazy danych i bez prawdziwego systemu plików. I to nie „oszustwo", ale normalna inżynierska ekonomia: testujesz dokładnie to, co chcesz utrwalić.

Wewnętrzne szczegóły (jak TaskService szuka zadania w repozytorium in-memory, jak storage zapisuje plik na dysk) — to już inna strefa odpowiedzialności. Może mieć własne testy, ale w naszym kursie nie są one główną osią. Teraz chronimy warstwę „umów z zewnętrznym światem".

3. @WebMvcTest vs @SpringBootTest

W ekosystemie Spring Boot jest kilka sposobów pisania testów i główna pułapka nowicjusza brzmi tak: „Widziałem @SpringBootTest, więc to jest właściwa droga". Mniej więcej jak „widziałem młot kowalski, więc każdy gwóźdź to jego robota". Młot kowalski jest oczywiście wszechstronny, ale czasem chciałeś tylko powiesić obraz.

@SpringBootTest podnosi pełny kontekst aplikacji. Przydaje się, gdy naprawdę sprawdzasz integrację warstw: kontroler → serwis → repozytorium → konfiguracja → infrastruktura. Ale pełny kontekst ma swoją cenę: wolniejszy start, więcej zależności, więcej powodów, dla których test może padać z powodu nieistotnych szczegółów. Dla kursu o REST API design zamienia się to w „szum zamiast sygnału".

@WebMvcTest — to inny styl myślenia. To web-layer slice test: tryb testowy, który podnosi tylko część MVC aplikacji (kontrolery, konwertery, walidację, handlery błędów itd.) i daje MockMvc, żebyś mógł wysyłać żądania i sprawdzać odpowiedzi. Jest specjalnie stworzony dla przypadków, gdy interesuje cię właśnie kontrakt HTTP/JSON.

Wygodnie zapamiętać to przez porównanie:

Adnotacja Co jest podnoszone Co testujemy Cena (czas/szum)
@WebMvcTest warstwa web + infrastruktura MVC kontrakt HTTP kontrolerów zazwyczaj szybko i lokalnie
@SpringBootTest cały kontekst aplikacji integrację warstw „jak w prawdziwym uruchomieniu" zazwyczaj wolniej i ciężej

I właśnie dlatego w tym kursie wybieramy @WebMvcTest jako główny tryb: naszym celem jest potwierdzenie, że zewnętrzny kontrakt odpowiada temu, co projektowaliśmy przez cały kurs. Nie budujemy tu „środowiska produkcyjnego", ale testy chroniące umowy API.

Dla kontrastu pokażę minimalny „pełny" szkielet testowy. Nie jest zły, po prostu ma inne skale:

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;

// Podnosimy CAŁY kontekst aplikacji (droższy czasowo, więcej zależności).
@SpringBootTest
// Podłączamy MockMvc ponad pełnym kontekstem.
@AutoConfigureMockMvc
class TaskControllerFullContextTest {
    // Tu zazwyczaj będą @Autowired MockMvc i/lub integracyjne sprawdzenia.
}

A oto @WebMvcTest — właśnie ten „wąski reflektor", który świeci tam, gdzie trzeba:

import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;

import com.example.tasktracker.api.controller.TaskController;

// Podnosimy tylko wycinek MVC wokół konkretnego kontrolera.
@WebMvcTest(TaskController.class)
class TaskControllerWebMvcTest {
    // Tu testujemy kontrakt HTTP/JSON, a nie serwisy/repozytoria/infrastrukturę.
}

Krótko mówiąc: @SpringBootTest sprawdza „jak system działa w całości", a @WebMvcTest sprawdza „jak drzwi rozmawiają z klientem". W tym kursie uczyliśmy właśnie „języka drzwi".

4. Co podnosi @WebMvcTest

Słowo „slice" brzmi trochę gastronomicznie, ale idea jest prosta: bierzemy aplikację i podnosimy w teście tylko ten kawałek, który dotyczy warstwy web. To nie magia, ale świadome ograniczenie kontekstu Spring. I właśnie dlatego testy są szybkie i sfokusowane.

Wewnątrz @WebMvcTest zazwyczaj znajdą się rzeczy, bez których MVC po prostu nie działa: DispatcherServlet, mechanika routingu, argument resolvers rozumiejące @PathVariable, @RequestParam, @RequestBody, HttpMessageConverter dla JSON, Bean Validation a także twoje kontrolery i @ControllerAdvice, jeśli uczestniczą w jednolitym kontrakcie błędów.

Serwisy, repozytoria i komponenty infrastrukturalne domyślnie nie trafiają do środka. To właśnie jest główna granica slice: kontroler i pipeline MVC pozostają prawdziwe, a zależności kontrolera w teście muszą być zarządzalne.

Dobry obraz mentalny dla @WebMvcTest w naszym projekcie wygląda tak:

flowchart TD
  T["Test JUnit Jupiter"] --> M["MockMvc"]
  M --> D["DispatcherServlet (pipeline MVC)"]
  D --> C["TaskController"]
  C --> S["TaskService (mock)"]
  C --> J["Jackson + HttpMessageConverter"]
  C --> V["Bean Validation"]
  D --> A["@ControllerAdvice (ProblemDetail)"]
  A --> M

W tym schemacie celowo nie ma repozytorium in-memory i lokalnego file storage. W ramach @WebMvcTest interesuje nas jedna rzecz: kontroler prawidłowo zamienia żądanie na wywołanie serwisu i z powrotem w odpowiedź HTTP, stosując walidację i jednolity error handling. Jeśli test potrzebuje już dysku, pełnego zestawu serwisów i infrastruktury, to dobry sygnał, że wyszedłeś na inny poziom weryfikacji.

5. MockMvc: HTTP bez prawdziwego portu

Słowo MockMvc czasem wprowadza w błąd: wydaje się, że to jakaś „nieprawdziwa imitacja", której nie można ufać. W praktyce MockMvc to bardzo uczciwy narzędzie. Nie podnosi prawdziwego serwera (Tomcat/Jetty) i nie robi sieciowego żądania przez TCP, ale przy tym przepuszcza twoje żądanie przez prawdziwą mechanikę MVC: mapowania, binding, konwersję wiadomości, walidację, obsługę wyjątków. Czyli wszystko dotyczące warstwy web działa „naprawdę".

W efekcie dostajesz prawie idealną sytuację dla nauki i ochrony kontraktu: test jest szybki, przewidywalny, nie zależy od wolnego portu na twojej maszynie i nie wymaga „wszystko podnieść, wszystko skonfigurować". A sprawdzasz dokładnie to, co chciałby zobaczyć klient: status, nagłówki, content type, JSON.

Najprostszy test zazwyczaj wygląda tak: wysyłasz żądanie i sprawdzasz status. Tak, to jeszcze nie pełna weryfikacja kontraktu (do nagłówków i kształtu JSON dojdziemy później), ale to świetny pierwszy krok do myślenia „jak klient HTTP".

import org.junit.jupiter.api.Test;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@Test
void listTasks_returns200() throws Exception {
    // mockMvc jest zazwyczaj wstrzykiwany z kontekstu @WebMvcTest — to nasz „wirtualny klient HTTP".
    mockMvc.perform(get("/api/v1/tasks"))
            // Utrwalamy część kontraktu: endpoint powinien odpowiadać 200 OK.
            .andExpect(status().isOk());
}

Ważne jest zauważenie stylu: nie wywołujemy metody kontrolera bezpośrednio jak zwykłej funkcji Java. Naprawdę modelujemy wejście z zewnątrz. I właśnie dlatego takie testy są przydatne: chronią API przed „przypadkową ewolucją" i zmuszają do myślenia kategoriami kontraktu.

I tak, MockMvc to właśnie miejsce, gdzie wygodnie sprawdzać drobiazgi, o których zawsze zapomina się przy ręcznej weryfikacji. Na przykład Location dla 201 Created albo Content-Disposition przy pobieraniu. W Postmanie to zobaczysz, ale nie zawsze pamiętasz sprawdzić ponownie. W teście utrwalasz to raz — i dalej żyjesz spokojniej.

6. Szkielet slice-testu TaskController

Żeby @WebMvcTest nie był abstrakcją „gdzieś tam", ale realnym narzędziem w projekcie, przydatne jest zobaczenie minimalnego szkieletu. Pokazuje on od razu główne: kontroler i pipeline MVC są podnoszone prawdziwie, a zależność serwisowa jest zastępowana zarządzalnym beanem.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import com.example.tasktracker.api.controller.TaskController;
import com.example.tasktracker.domain.service.TaskService;

// Podnosimy wycinek MVC tylko dla TaskController.
@WebMvcTest(TaskController.class)
class TaskControllerWebMvcTest {

    // Główne narzędzie do „żądań HTTP" bez prawdziwego portu.
    @Autowired
    MockMvc mockMvc;

    // Zależność kontrolera zastępujemy mockiem, żeby test był deterministyczny.
    @MockitoBean
    TaskService taskService;
}

Ten szkielet już wystarczy, żeby zobaczyć mechanikę: MockMvc wysyła żądania przez prawdziwy pipeline MVC, a warstwa serwisowa jest zastąpiona zarządzalnym beanem. Na tym samym szkielecie dalej trzymają się thenReturn/thenThrow, praca z ciałem JSON przez ObjectMapper i wszystkie asercje kontraktu.

7. Granica @WebMvcTest

To narzędzie ma bardzo wyraźną granicę. Służy do ochrony zewnętrznego kontraktu HTTP, a nie do jednoczesnego dowodzenia prawidłowości całej aplikacji.

Tu warto sprawdzać statusy, nagłówki, content type, kształt JSON, walidację i jednolity ProblemDetail.

Tu nie warto dowodzić, jak repozytorium in-memory sortuje dane, jak storage zapisuje plik na dysk albo jak inicjalizowane są dane startowe.

Jeśli test potrzebuje już prawdziwego dysku, pełnego zestawu serwisów i infrastruktury, wyszedłeś na inny poziom weryfikacji. @WebMvcTest nie staje się przez to „słabym" — po prostu trzyma fokus tam, gdzie kontrakt jest naprawdę widoczny dla klienta.

8. Typowe błędy na początku z @WebMvcTest

Gdy deweloper po raz pierwszy zaczyna pisać slice testy MVC, często czuje, że Spring „marudzi": to kontekst nie zebrał się, to jakaś zależność nie jest znaleziona, to test dziwnie pada przy pozornie prostym żądaniu. To normalny etap przyzwyczajania się i prawie zawsze sprowadza się do kilku powtarzalnych błędów.

Błąd №1: używanie @SpringBootTest „z przyzwyczajenia", a potem dziwienie się, że wszystko długo i głośno.
Jeśli celem testu jest sprawdzenie, że kontroler zwraca prawidłowy status/nagłówek/kształt JSON, pełny kontekst zazwyczaj przeszkadza bardziej niż pomaga. Zaczynasz naprawiać konfigurację całej aplikacji, choć chciałeś sprawdzić jedne drzwi. W efekcie testy są wolne, a padania — nieoczywiste: problem może być w ogóle nie w kontrolerze, ale w jakimś beanie infrastrukturalnym, który nie jest ci w danym teście wcale potrzebny.

Błąd №2: próba sprawdzenia przez @WebMvcTest działania serwisu, repozytorium i przechowalni plików jednocześnie.
To bardzo częsta psychologiczna pułapka: wydaje się, że „im więcej sprawdzę za jednym razem, tym lepiej". W praktyce dostajesz test, który źle lokalizuje problem. Padł test — i nie wiadomo, co się zepsuło: routing, mapowanie JSON, walidacja, logika serwisowa czy praca systemu plików. Sens testu slice polega właśnie na trzymaniu fokusu na warstwie web i nie mieszaniu poziomów odpowiedzialności.

Błąd №3: wywoływanie metod kontrolera bezpośrednio jak zwykłych metod Java i uważanie tego za „test API".
Jeśli wywołujesz taskController.listTasks() bezpośrednio, pomijasz połowę tego, co robi Spring MVC: bindowanie parametrów, czytanie @RequestBody, pracę walidatora, konwersję wiadomości, @ControllerAdvice. Taki test może być użyteczny jako unit test kontrolera (choć kontrolery u nas są cienkie), ale nie sprawdza zewnętrznego kontraktu tak, jak widzi go klient HTTP. W ramach tego kursu chcemy właśnie „kliencki punkt widzenia", więc MockMvc to główne narzędzie.

Błąd №4: sprawdzanie w testach tylko statusu, ignorowanie nagłówków i kształtu JSON.
Sprawdzanie status().isOk() — to dobry start, ale jeśli zatrzymać się tylko na tym, testy będą zbyt słabe. Kontrakt łamie się najczęściej właśnie w szczegółach: zmienił się Location, zapomniano Content-Type, zmienił się kształt odpowiedzi, zepsuł się ProblemDetail. Tych błędów „nie widać" po samym kodzie statusu. Dlatego testy warstwy web mają wartość właśnie wtedy, gdy mogą utrwalać kształt odpowiedzi.

1
Zadanie
Spring REST & MVC, poziom 30, lekcja 0
Niedostępne
Minimalny `@WebMvcTest` dla health endpoint
Minimalny `@WebMvcTest` dla health endpoint
1
Zadanie
Spring REST & MVC, poziom 30, lekcja 0
Niedostępne
`@WebMvcTest` dla endpointu z parametrem zapytania
`@WebMvcTest` dla endpointu z parametrem zapytania
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION