@WebMvcTest

Starts only the Spring MVC layer for chosen controllers, with MockMvcTester ready, so controller tests run fast without services or a database.

Signature
@WebMvcTest(OrderController.class)
  • Added in Spring Boot Spring Boot 1.4

Parameters

ParameterTypeDescription
$controllers / valueoptionalClass<?>[]= {}The controllers to include. Empty means every controller in the application.
$propertiesoptionalString[]= {}key=value pairs added to the Environment for this test.
$useDefaultFiltersoptionalboolean= trueWhether to use the default type filter that limits scanning to MVC types.
$includeFiltersoptionalComponentScan.Filter[]= {}Extra types to add to the slice.
$excludeFiltersoptionalComponentScan.Filter[]= {}Types to remove from the slice.
$excludeAutoConfigurationoptionalClass<?>[]= {}Auto-configuration classes to leave out.

Return value

n/aStarts a Spring MVC test slice: the named controllers plus MVC infrastructure, with MockMvc and MockMvcTester auto-configured and no services, repositories or database.

Errors

The context fails to start if a scanned controller depends on a bean that is not in the slice and is not mocked or imported.

Changelog

  • PHP Spring Boot 4.0Moved to org.springframework.boot.webmvc.test.autoconfigure in the spring-boot-webmvc-test module (spring-boot-starter-webmvc-test).

@WebMvcTest is a test slice: it starts a small application context with only what the MVC layer needs. That means @Controller and @RestController classes, @ControllerAdvice, filters, WebMvcConfigurer beans, converters and the JSON setup. It does not scan @Service, @Repository or plain @Component classes, and it starts no database. A MockMvcTester (and a classic MockMvc) is registered for you.

In Spring Boot 4 the annotation lives in org.springframework.boot.webmvc.test.autoconfigure and comes from spring-boot-starter-webmvc-test. The Boot 3 import, org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest, no longer compiles.

What it is for

A controller test answers questions the service tests cannot: does this URL map to that method, does the JSON bind into the record, does @Valid turn a bad body into a 400, does the advice turn a domain exception into the right Problem Details, is the response JSON shaped the way clients expect. Everything below the controller is replaced with @MockitoBean mocks so each test controls exactly what the service returns.

Name the controller

Without arguments @WebMvcTest loads every controller in the application. Each of those needs its dependencies, so one test class ends up mocking services it never calls, and every new controller breaks unrelated tests with No qualifying bean of type. Pass the controller under test: @WebMvcTest(OrderController.class).

Common mistakes

  • Mocking a mapper or formatter that is a pure function. @Import the real one instead; a mock that returns whatever you told it proves nothing about the JSON.
  • Expecting a plain @Component helper to appear. Only the types listed above are scanned (servlet Filter beans are among them, so a filter will run); anything else must be imported or mocked.
  • Forgetting security. With Spring Security on the classpath, its auto-configuration applies to the slice, so an unauthenticated request gets 401 or 403. Use spring-security-test support such as @WithMockUser, and import your SecurityFilterChain configuration if the test should use your rules rather than the defaults.
  • Testing business rules here. If the assertion is about a calculation, it belongs in a unit test of the service.

A slice context is small and cached like any other, so a few hundred controller tests still run in seconds.

Example

Example · java
import static org.mockito.BDDMockito.given;

import java.math.BigDecimal;
import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

record OrderView(long id, String status, BigDecimal total) {}

@Service
class OrderService {
    Optional<OrderView> find(long id) {
        throw new UnsupportedOperationException("talks to the database");
    }
}

@RestController
@RequestMapping("/api/orders")
class OrderController {

    private final OrderService orders;

    OrderController(OrderService orders) {
        this.orders = orders;
    }

    @GetMapping("/{id}")
    ResponseEntity<OrderView> find(@PathVariable long id) {
        return ResponseEntity.of(orders.find(id));
    }
}

// Only the MVC layer for OrderController; the service is a mock.
@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    MockMvcTester mvc;

    @MockitoBean
    OrderService orders;

    @Test
    void returnsTheOrder() {
        given(orders.find(42L)).willReturn(Optional.of(new OrderView(42L, "PAID", new BigDecimal("19.90"))));

        mvc.get().uri("/api/orders/42")
                .assertThat()
                .hasStatusOk()
                .bodyJson()
                .extractingPath("$.status").isEqualTo("PAID");
    }

    @Test
    void unknownOrderIs404() {
        given(orders.find(7L)).willReturn(Optional.empty());

        mvc.get().uri("/api/orders/7")
                .assertThat()
                .hasStatus(HttpStatus.NOT_FOUND);
    }
}

When to use it

  • Check that GET /api/orders/{id} returns 404 when the service finds nothing and 200 with the right JSON when it does.
  • Prove that a POST with a missing required field is rejected with 400 before the service is called.
  • Pin the Problem Details body the advice produces for each domain exception clients handle.

Frequently asked questions

Why does @WebMvcTest fail with 'No qualifying bean of type ...Service'?

Services are not part of the slice. Add a @MockitoBean field for each collaborator the controller needs, or @Import the real class if it is a simple helper. If the missing bean belongs to a different controller, name the controller under test in @WebMvcTest(...).

What is the Spring Boot 4 import for @WebMvcTest?

org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest, from the spring-boot-starter-webmvc-test dependency. The Boot 3 package org.springframework.boot.test.autoconfigure.web.servlet no longer exists.

Can I use classic MockMvc instead of MockMvcTester?

Yes. Both beans are registered, so you can inject MockMvc and use perform() with ResultMatchers. MockMvcTester is the AssertJ-style API and gives clearer failure messages, so prefer it in new tests.

More examples

Proving validation turns a bad body into 400

Validation runs in the MVC layer, so a slice test is the cheapest place to prove that <code>@Valid</code> is on the parameter and the constraints are on the record. This controller has no dependencies, so nothing needs mocking. A text block keeps the JSON readable and is sent exactly as a client would send it.

Example · java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

record SignUp(@NotBlank String name, @Email @NotBlank String email) {}

@RestController
class SignUpController {

    @PostMapping("/api/sign-ups")
    ResponseEntity<Void> signUp(@Valid @RequestBody SignUp request) {
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
}

@WebMvcTest(SignUpController.class)
class SignUpControllerTest {

    @Autowired
    MockMvcTester mvc;

    @Test
    void rejectsAnInvalidEmail() {
        mvc.post().uri("/api/sign-ups")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                        {"name": "Ada", "email": "not-an-email"}
                        """)
                .assertThat()
                .hasStatus(HttpStatus.BAD_REQUEST);
    }

    @Test
    void acceptsAValidSignUp() {
        mvc.post().uri("/api/sign-ups")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                        {"name": "Ada", "email": "[email protected]"}
                        """)
                .assertThat()
                .hasStatus(HttpStatus.CREATED);
    }
}

The slice includes your @RestControllerAdvice

Advice classes are picked up by the slice without any import, which is what makes <code>@WebMvcTest</code> the right place to test error responses. The test checks the status, the <code>application/problem+json</code> content type and a field of the body, which together are the contract your clients rely on.

Example · java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;

class CustomerNotFoundException extends RuntimeException {
    CustomerNotFoundException(long id) {
        super("Customer " + id + " not found");
    }
}

@RestController
class CustomerController {

    @GetMapping("/api/customers/{id}")
    String find(@PathVariable long id) {
        throw new CustomerNotFoundException(id);
    }
}

// Advice classes are part of the slice, so this one is loaded automatically.
@RestControllerAdvice
class ApiErrors {

    @ExceptionHandler
    ProblemDetail notFound(CustomerNotFoundException ex) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
    }
}

@WebMvcTest(CustomerController.class)
class CustomerErrorsTest {

    @Autowired
    MockMvcTester mvc;

    @Test
    void missingCustomerIsAProblemDetail() {
        mvc.get().uri("/api/customers/9")
                .assertThat()
                .hasStatus(HttpStatus.NOT_FOUND)
                .hasContentType("application/problem+json")
                .bodyJson()
                .extractingPath("$.detail").isEqualTo("Customer 9 not found");
    }
}

Importing a real helper instead of mocking it

A <code>@Component</code> outside the MVC types is not in the slice, so the controller cannot start until you supply it. For pure, fast helpers such as formatters and mappers, <code>@Import</code> the real class: the test then checks the actual output rather than a value you stubbed yourself.

Example · java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

// A plain @Component: the slice does NOT scan it.
@Component
class PriceFormatter {
    String format(long pence) {
        return String.format("GBP %d.%02d", pence / 100, pence % 100);
    }
}

@RestController
class PriceController {

    private final PriceFormatter formatter;

    PriceController(PriceFormatter formatter) {
        this.formatter = formatter;
    }

    @GetMapping("/api/prices/format")
    String format(@RequestParam long pence) {
        return formatter.format(pence);
    }
}

// Pull the real formatter in, rather than mocking a pure function.
@WebMvcTest(PriceController.class)
@Import(PriceFormatter.class)
class PriceControllerTest {

    @Autowired
    MockMvcTester mvc;

    @Test
    void formatsPence() {
        mvc.get().uri("/api/prices/format?pence=1999")
                .assertThat()
                .hasStatusOk()
                .hasBodyTextEqualTo("GBP 19.99");
    }
}

Discussion

  • Be the first to comment on this lesson.