@WebMvcTest & MockMvc
@WebMvcTest is a slice that boots only the Spring MVC layer — controllers, filters, @ControllerAdvice, JSON converters — and nothing else. No services, no repositories, no database. You drive it with MockMvc, which executes requests through the full MVC machinery without starting a real HTTP server. The result is a fast, focused test of your web layer.
What @WebMvcTest loads
Pointing the annotation at a single controller keeps the slice tiny and the test fast.
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired
MockMvc mockMvc; // auto-configured by the slice
@MockitoBean
ProductService service; // the service layer is NOT loaded — we mock it
}
Because the service layer is excluded from the slice, you must provide it as a mock. @MockitoBean registers a Mockito mock in the test context, replacing the missing bean. (It is the Spring Boot 3.4+ replacement for the deprecated @MockBean.)
Performing requests and asserting
mockMvc.perform(...) sends a simulated request; andExpect(...) asserts on the result. jsonPath reads into the JSON body.
@Test
void returnsProductById() throws Exception {
when(service.findById(1L))
.thenReturn(Optional.of(new Product(1L, "Keyboard", new BigDecimal("49.90"))));
mockMvc.perform(get("/api/products/1"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("Keyboard"));
}
Static imports come from MockMvcRequestBuilders (get, post, …) and MockMvcResultMatchers (status, jsonPath, content).
Testing a POST with a JSON body
Serialize the request body and assert the created status and Location header.
@Test
void createsProduct() throws Exception {
var saved = new Product(7L, "Mouse", new BigDecimal("19.90"));
when(service.create(any())).thenReturn(saved);
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{ "name": "Mouse", "price": 19.90 }
"""))
.andExpect(status().isCreated())
.andExpect(header().string("Location", "/api/products/7"))
.andExpect(jsonPath("$.id").value(7));
}
Output:
MockHttpServletResponse:
Status = 201
Content type = application/json
Body = {"id":7,"name":"Mouse","price":19.90}
Headers = [Location:"/api/products/7"]
PASSED
Testing 404 and error paths
Stub the service to return empty and assert the controller maps it to 404.
@Test
void returns404WhenMissing() throws Exception {
when(service.findById(999L)).thenReturn(Optional.empty());
mockMvc.perform(get("/api/products/999"))
.andExpect(status().isNotFound());
}
If your controller relies on a @RestControllerAdvice, it is loaded by the slice, so exception-to-status mapping is tested end-to-end within the web layer. See @RestControllerAdvice.
Testing validation
@WebMvcTest loads the Bean Validation infrastructure, so @Valid constraints fire. Send an invalid body and assert 400 plus the field errors.
@Test
void rejectsBlankName() throws Exception {
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{ "name": "", "price": -5 }
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.name").value("must not be blank"))
.andExpect(jsonPath("$.price").value("must be greater than 0"));
}
Tip:
@WebMvcTestdoes not include@Serviceor@Repositorybeans, only@Controller/@RestController. If your test fails because a service bean is missing, that is the slice working as designed — add a@MockitoBeanfor it.
MockMvcTester — the AssertJ-native API
Spring Framework 6.2 / Spring Boot 3.4 added MockMvcTester, a fluent wrapper that integrates with AssertJ so you can chain assertThat(...) instead of andExpect(...).
@WebMvcTest(ProductController.class)
class ProductControllerTesterTest {
@Autowired MockMvcTester mvc;
@MockitoBean ProductService service;
@Test
void returnsProduct() {
when(service.findById(1L))
.thenReturn(Optional.of(new Product(1L, "Keyboard", BigDecimal.TEN)));
assertThat(mvc.get().uri("/api/products/1"))
.hasStatusOk()
.bodyJson().extractingPath("$.name").isEqualTo("Keyboard");
}
}
Common matchers
| Matcher | Asserts |
|---|---|
status().isOk() / .isCreated() / .isNotFound() | HTTP status code |
jsonPath("$.field").value(x) | A JSON value |
jsonPath("$.items", hasSize(3)) | Collection size (Hamcrest) |
content().contentType(APPLICATION_JSON) | Response content type |
header().string("Location", path) | A response header |
content().string(...) | Raw response body |
Note: Authentication filters can interfere with
@WebMvcTest. Addspring-security-testand annotate requests withwith(user("alice")), or disable security for the slice when you only care about the controller logic.