Have you ever felt frustrated when writing Controller tests with @SpringBootTest — needing to configure a database or waiting through slow startup times? If you only need to verify the HTTP layer, using @WebMvcTest lets you write tests that are simpler and much faster.

In this post, I’ll walk through how to write Controller-specific tests by combining @WebMvcTest with MockMvc, complete with implementation examples. Basic knowledge of JUnit and Mockito is assumed (see Introduction to Spring Boot Testing with JUnit and Mockito for the fundamentals).

Differences Between @WebMvcTest and @SpringBootTest

@SpringBootTest starts the entire application context, including databases, message queues, and everything else. It’s useful for integration tests, but it’s clearly overkill when you just want to verify a Controller.

@WebMvcTest starts only the web layer (Controllers, Filters, Converters, etc.). Services and Repositories are not included in the context, so you mock them with @MockBean.

@SpringBootTest@WebMvcTest
Startup scopeEntire applicationWeb layer only
SpeedSlowFast
DB connectionOften requiredNot needed
Use caseIntegration testingController unit testing

For verifying only the HTTP layer, use @WebMvcTest. For integration tests involving the database, the combination of @SpringBootTest and Testcontainers is the right fit.

Setting Up the Controller Under Test

First, let’s prepare a simple UserController to test against.

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
        return ResponseEntity.ok(userService.findById(id));
    }

    @PostMapping
    public ResponseEntity<Void> createUser(@Valid @RequestBody UserRequest request) {
        Long id = userService.create(request);
        return ResponseEntity.created(URI.create("/users/" + id)).build();
    }
}

Basic Setup for @WebMvcTest

Here’s how to write the test class.

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @MockBean
    UserService userService;
}

There are three key points:

  • @WebMvcTest(UserController.class) specifies the Controller to test
  • MockMvc is auto-configured when using @WebMvcTest, so you just inject it with @Autowired
  • UserService is mocked with @MockBean. Since Services are not included in the context, omitting this will cause a NoSuchBeanDefinitionException at startup

Testing GET Requests

Pass a request to perform() and chain your assertions with andExpect().

@Test
void canFetchUserById() throws Exception {
    // Arrange
    UserResponse user = new UserResponse(1L, "Taro", "[email protected]");
    when(userService.findById(1L)).thenReturn(user);

    // Act & Assert
    mockMvc.perform(get("/users/1"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.id").value(1))
        .andExpect(jsonPath("$.name").value("Taro"))
        .andExpect(jsonPath("$.email").value("[email protected]"));
}

Adding static imports keeps things clean.

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.mockito.Mockito.*;
import static org.hamcrest.Matchers.*;

jsonPath supports nested fields as well. For array elements, use $.items[0].name; to check the number of items in a list, jsonPath("$.items", hasSize(3)) is handy.

Testing POST Requests

When sending a request body, add .content() and .contentType().

@Test
void canCreateUser() throws Exception {
    // Arrange
    UserRequest request = new UserRequest("Hanako", "[email protected]");
    when(userService.create(any())).thenReturn(2L);

    // Act & Assert
    mockMvc.perform(post("/users")
            .content(objectMapper.writeValueAsString(request))
            .contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isCreated())
        .andExpect(header().string("Location", "/users/2"));
}

ObjectMapper is automatically provided in the @WebMvcTest context, so you can inject it with @Autowired.

Testing Validation Errors (400 Bad Request)

When input validation is configured with @Valid, sending an invalid request returns a 400. Make sure to cover this pattern as well.

@Test
void invalidRequestReturns400() throws Exception {
    // Validation error due to empty name
    UserRequest request = new UserRequest("", "[email protected]");

    mockMvc.perform(post("/users")
            .content(objectMapper.writeValueAsString(request))
            .contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isBadRequest());
}

If you have a custom error response format, you can validate field-by-field with something like jsonPath("$.errors[0].field").value("name") (see ExceptionHandler for REST APIs). For custom validation annotations, check out this post as well.

Verifying Service Calls

You can use verify() to confirm that the Controller calls the Service correctly.

@Test
void callsFindByIdOnServiceOnce() throws Exception {
    when(userService.findById(1L))
        .thenReturn(new UserResponse(1L, "Taro", "[email protected]"));

    mockMvc.perform(get("/users/1"))
        .andExpect(status().isOk());

    verify(userService, times(1)).findById(1L);
}

To verify that a method was never called, use verify(userService, never()).delete(any()).

With Spring Security

Adding spring-security-test lets you send requests as an authenticated user via @WithMockUser.

@Test
@WithMockUser(username = "admin", roles = "ADMIN")
void adminReceives200() throws Exception {
    mockMvc.perform(get("/admin/users"))
        .andExpect(status().isOk());
}

Since @WebMvcTest also loads Security auto-configuration, accessing an endpoint without authentication will return a 401 or 302. Refer to the dedicated documentation for detailed Security test configuration.

Summary

With @WebMvcTest, you can quickly test just the HTTP layer of your Controllers without starting a database or external services. Mastering the pattern of mocking Services with @MockBean and verifying with MockMvc’s perform/andExpect chain will significantly improve the quality of your Controllers.

For integration tests involving database connections, check out @SpringBootTest with Testcontainers. For mocking external APIs, take a look at WireMock as well.