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 scope | Entire application | Web layer only |
| Speed | Slow | Fast |
| DB connection | Often required | Not needed |
| Use case | Integration testing | Controller 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 testMockMvcis auto-configured when using@WebMvcTest, so you just inject it with@AutowiredUserServiceis mocked with@MockBean. Since Services are not included in the context, omitting this will cause aNoSuchBeanDefinitionExceptionat 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.