Hire Me
← All Writing Spring Boot

Testing REST Controllers with @WebMvcTest and MockMvc

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.

@WebMvcTest vs @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.

Basic setup

@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.

Testing 404 and error responses

@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.

Testing POST with a request body

@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.

Bean validation testing

@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.

Security configuration

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.

Capturing and logging the response

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.

Testing content negotiation

@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.

What @WebMvcTest does not cover

The 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.

Samuel Jackson

Samuel Jackson

Senior Java Back End Developer & Contractor

Senior Java Back End Developer — Betfair Exchange API specialist, Spring Boot, AWS, and event-driven architecture. 20+ years delivering high-performance systems across betting, finance, energy, retail, and government. Available for Java contracting.