Mastering Spring Boot Controller Tests: 8 Powerful Techniques
This article presents a comprehensive guide to testing Spring Boot controller layers, covering isolated @WebMvcTest, full‑stack @SpringBootTest, WebTestClient, TestRestTemplate, standalone MockMvc, REST Assured, exception‑handling advice, and pure Mockito unit tests, each with purpose, core annotations, sample code, pros, cons, and usage recommendations.
1. Introduction
In a front‑back separation architecture, the Controller acts as the façade, handling requests, parameters, invoking business logic and returning responses. Its stability directly affects system availability and user experience.
Systematic controller interface testing is essential to verify API correctness, data exchange accuracy, and to expose integration defects early, ensuring expected behavior under various edge cases and supporting continuous delivery.
Typical validations performed on the Controller layer
Request mapping – verify HTTP methods and paths are correctly mapped to target methods.
Request/response serialization – ensure JSON ↔ Java object conversion works ( @RequestBody, @ResponseBody ).
Validation, exception handling and status codes – test data validation ( @Valid ), global exception handlers ( @ControllerAdvice ) return proper 400/404/500 codes.
Integration with service layer – verify the controller calls Service methods with correct parameters, using @MockBean or real services.
2. Practical examples
2.1 Using @WebMvcTest
Purpose : Isolate the controller by mocking all dependencies, allowing fast, focused tests of request mapping, parameter binding, JSON serialization, status codes and interaction with mocked services.
Key annotations & classes :
@WebMvcTest(YourController.class) – loads only the web layer.
MockMvc – simulates HTTP requests without a real server.
@MockBean – replaces real beans (e.g., UserService) with Mockito mocks.
Test case :
@WebMvcTest(UserController.class)
public class UserControllerTest1 {
@Resource
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
public void testUsers() throws Exception {
when(userService.queryUsers()).thenReturn(List.of(new User(1L, "pack", 33)));
mockMvc.perform(get("/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").value("pack"));
}
@Test
public void testUser() throws Exception {
when(userService.queryUser(1L)).thenThrow(new UserNotFoundException());
mockMvc.perform(get("/users/1"))
.andExpect(status().isNotFound());
}
}Pros : fast execution, isolated controller logic, easy verification of request/response format.
Cons : does not test real service or data‑layer integration, limited Spring context validation.
When to use : quick, focused controller tests, validation of HTTP responses and error handling without full application startup.
2.2 @SpringBootTest + MockMvc (full integration)
Purpose : Test the entire stack – controller, service, repository, configuration, security filters, etc.
Core annotations :
@SpringBootTest – starts the complete Spring application context.
@AutoConfigureMockMvc – enables MockMvc for HTTP testing without a real server.
Test case :
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest2 {
@Resource
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
public void testSave() throws Exception {
String jsonBody = """{"id":2,"name":"admin","age":33}""";
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonBody))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("admin"));
}
}Pros : validates complete workflow, catches integration issues, mirrors real runtime behavior.
Cons : slower, requires test database configuration, harder to pinpoint failures.
When to use : end‑to‑end API tests, when mock‑based tests are insufficient (transactions, security, database constraints).
2.3 Using WebTestClient
WebTestClient works for both Spring MVC and WebFlux, offering a fluent API for request/response verification.
Test case :
@WebFluxTest(UserController.class)
public class UserControllerTest3 {
@Resource
private WebTestClient webTestClient;
@MockBean
private UserService userService;
@Test
public void testQueryUsers() throws Exception {
when(userService.queryUsers()).thenReturn(List.of(new User(1L, "pack", 33)));
webTestClient.get().uri("/users")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("pack");
}
}Pros : unified support for MVC and WebFlux, readable fluent style, supports streaming responses.
Cons : newer, some teams prefer MockMvc; slightly less fine‑grained control for pure MVC.
When to use : reactive applications, or when a single tool for both MVC and WebFlux is desired.
2.4 TestRestTemplate
TestRestTemplate sends real HTTP requests to a running Spring Boot instance, testing the full server stack including filters, security, and CORS.
Test case :
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class UserControllerTest4 {
@Resource
private TestRestTemplate restTemplate;
@Test
public void testQueryUsers() throws Exception {
ResponseEntity<User[]> response = restTemplate.getForEntity("/users", User[].class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals("pack", response.getBody()[0].name());
}
}Pros : tests real HTTP behavior, suitable for API gateways, security filters, and external service integration.
Cons : slower, requires full server startup, harder to debug network‑related failures.
2.5 Standalone MockMvc (no Spring context)
Creates the controller manually and uses MockMvcBuilders.standaloneSetup() for ultra‑fast unit tests.
Test case :
public class UserControllerTest5 {
private MockMvc mockMvc;
private UserService userService = mock(UserService.class);
@BeforeEach
public void setup() {
UserController controller = new UserController(userService);
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
}
@Test
public void testQueryUsers() throws Exception {
when(userService.queryUsers()).thenReturn(List.of(new User(1L, "pack", 33)));
mockMvc.perform(get("/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").value("pack1"));
}
}Pros : fastest execution, full control over dependencies, ideal for TDD.
Cons : cannot test Spring features such as @Valid, @ControllerAdvice, security annotations.
2.6 REST Assured
REST Assured provides a BDD‑style DSL for testing any REST API, supporting complex JSON/XML assertions.
Test case :
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class UserControllerTest6 {
@LocalServerPort
private int port;
@Test
public void testQueryUsers() {
given().port(port)
.when().get("/users")
.then().statusCode(200)
.body("[0].name", equalTo("pack"))
.body("size()", greaterThan(1));
}
}Pros : readable BDD syntax, powerful assertions, works with external APIs.
Cons : extra dependency, slightly slower than MockMvc, learning curve.
2.7 Testing ControllerAdvice and exception handling
Ensures global exception handlers return the correct JSON structure and HTTP status.
Example advice :
@RestControllerAdvice
public class UserControllerAdvice {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<?> error(UserNotFoundException e) {
return ResponseEntity.status(404).body(Map.of("code", -1, "error", e.getMessage()));
}
}
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException() {}
public UserNotFoundException(String message) { super(message); }
public UserNotFoundException(String message, Throwable cause) { super(message, cause); }
}Test case :
@WebMvcTest(UserController.class)
public class UserControllerTest7 {
@Resource
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
public void testUser() throws Exception {
when(userService.queryUser(1L)).thenThrow(new UserNotFoundException("用户不存在"));
mockMvc.perform(get("/users/1"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.error").value("用户不存在"));
}
}Best practice : combine mocked exceptions with real validation failures to fully cover error paths.
2.8 Pure Mockito unit tests
Bypassing HTTP and Spring entirely, these tests focus on controller method logic and interaction with mocked services.
Test case :
public class UserControllerTest8 {
private UserService userService = mock(UserService.class);
private UserController userController = new UserController(userService);
@Test
public void testQueryUsers() throws Exception {
when(userService.queryUsers()).thenReturn(List.of(new User(1L, "pack", 33)));
List<User> users = userController.queryUsers();
assertEquals("pack", users.get(0).name());
verify(userService).queryUsers();
}
}Pros : fastest, full control over dependencies, ideal for complex controller logic.
Cons : does not test request mapping, serialization, or Spring‑specific features.
Sample controller and service used in the examples
public record User(Long id, String name, Integer age) {}
@Service
public class UserService {
public User queryUser(Long id) {
return new User(id, "Pack_xg", 33);
}
public List<User> queryUsers() {
return List.of(new User(1L, "pack", 33));
}
public User save(User user) {
System.err.println("创建用户...");
return user;
}
}
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public User queryUser(@PathVariable Long id) {
return this.userService.queryUser(id);
}
@GetMapping("")
public List<User> queryUsers() {
return this.userService.queryUsers();
}
@PostMapping("")
@ResponseStatus(code = HttpStatus.CREATED)
public User save(@RequestBody User user) {
return user;
}
}Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
