How to test Spring Boot REST controllers in isolation using @WebMvcTest and MockMvc — without starting a full server — covering request building, response assertions, and security wiring.
Controller tests are the fastest feedback loop in a Spring Boot service. They run without a real HTTP server, exercise the full request-handling stack — filters, argument resolution, validation, exception handling — and complete in milliseconds. Getting the setup right means you can test every important controller behaviour without the overhead of @SpringBootTest.
@SpringBootTest boots the entire application context. It finds all beans, starts all auto-configurations, and optionally binds a port. That fidelity has a cost: several seconds per test class, database connections established, downstream clients wired up.
@WebMvcTest loads only the web layer: controllers, @ControllerAdvice, filters, WebMvcConfigurer beans. It does not load @Service, @Repository, or any infrastructure beans. Dependencies of the controller must be provided as mocks.
Use @WebMvcTest for controller logic. Use @SpringBootTest for integration tests that cross component boundaries.
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
MockMvc mvc;
@MockBean
OrderService orderService;
@Test
void getOrder_returns200WithOrderJson() throws Exception {
OrderDto order = new OrderDto("ORD-001", "BACK", 2.5, 10.0, "ACTIVE");
given(orderService.findById("ORD-001")).willReturn(Optional.of(order));
mvc.perform(get("/orders/ORD-001")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value("ORD-001"))
.andExpect(jsonPath("$.side").value("BACK"))
.andExpect(jsonPath("$.price").value(2.5));
}
}
@MockBean creates a Mockito mock and registers it as a Spring bean, replacing any real implementation. given(...).willReturn(...) is Mockito BDD syntax — identical to when(...).thenReturn(...) but more readable in test contexts.
@Test
void getOrder_returns404WhenNotFound() throws Exception {
given(orderService.findById("MISSING")).willReturn(Optional.empty());
mvc.perform(get("/orders/MISSING")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.error").value("Order not found"));
}
For this to work, your @ControllerAdvice must handle the OrderNotFoundException and return a structured body. @WebMvcTest loads @ControllerAdvice beans — they are part of the web layer — so exception handling is tested accurately.
@Test
void placeOrder_returns201WithLocation() throws Exception {
PlaceOrderRequest req = new PlaceOrderRequest("1.234567", "789", 2.5, 10.0, "BACK");
OrderDto created = new OrderDto("ORD-002", "BACK", 2.5, 10.0, "PENDING");
given(orderService.placeOrder(req)).willReturn(created);
mvc.perform(post("/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"marketId": "1.234567",
"selectionId": "789",
"price": 2.5,
"size": 10.0,
"side": "BACK"
}
"""))
.andExpect(status().isCreated())
.andExpect(header().string("Location", containsString("/orders/ORD-002")))
.andExpect(jsonPath("$.id").value("ORD-002"));
}
Use Java text blocks for inline JSON rather than string concatenation — it reads cleanly and avoids escape noise.
@WebMvcTest wires @Valid and constraint annotations via the MethodValidationPostProcessor. Test that invalid input is rejected before it reaches the service:
@Test
void placeOrder_returns400WhenPriceNegative() throws Exception {
mvc.perform(post("/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"marketId": "1.234567",
"selectionId": "789",
"price": -1.0,
"size": 10.0,
"side": "BACK"
}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors[0].field").value("price"));
}
Ensure your @ControllerAdvice handles MethodArgumentNotValidException and returns the field-level errors in a predictable structure that your tests can assert against.
If Spring Security is on the classpath, @WebMvcTest loads the security filter chain by default. An unauthenticated request to a protected endpoint returns 401 or 302, not your controller’s response.
For controller tests focused on business logic, disable security for the test slice:
@WebMvcTest(controllers = OrderController.class,
excludeAutoConfiguration = SecurityAutoConfiguration.class)
Or include the security config but wire a test user:
@Test
@WithMockUser(roles = "TRADER")
void placeOrder_authenticatedUserCanPlaceOrder() throws Exception {
// ...
}
@WithMockUser requires spring-security-test on the classpath. It sets up a SecurityContext with a mock authentication, bypassing actual credential verification.
For debugging a failing test, andDo(print()) writes the full request and response to stdout:
mvc.perform(get("/orders/ORD-001"))
.andDo(print())
.andExpect(status().isOk());
Remove it before committing — it generates noise in CI output.
@Test
void getOrder_returns406ForUnsupportedMediaType() throws Exception {
mvc.perform(get("/orders/ORD-001")
.accept(MediaType.APPLICATION_XML))
.andExpect(status().isNotAcceptable());
}
@WebMvcTest respects produces declarations on @RequestMapping. If your controller only produces JSON, XML requests correctly return 406.
@MockBean or test the client separately@SpringBootTest with a real or embedded databaseThe boundaries are clear: @WebMvcTest owns HTTP mechanics (routing, serialisation, validation, error mapping). Everything below the controller is a mock.
If you’re working on a Spring Boot service and want a review of your controller testing strategy, get in touch.