In project development, testing is a key link in ensuring application quality. For web applications built on SpringBoot, efficient testing of the MVC layer can greatly improve development and joint debugging efficiency. A well-designed testing strategy can not only identify potential problems, but also improve code quality, promote system stability, and provide guarantees for subsequent reconstruction and functional expansion.
Solution 1: Use MockMvc for controller unit testing
How it works
MockMvc is a core class provided by the Spring Test framework. It allows developers to simulate HTTP requests and responses without starting an HTTP server and directly test the controller method. This method is fast and has good isolation, and is especially suitable for pure unit testing.
Implementation steps
Introduce dependencies
<dependency> <groupId></groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
Write a controller to be tested
@RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; public UserController(UserService userService) { = userService; } @GetMapping("/{id}") public ResponseEntity<UserDto> getUserById(@PathVariable Long id) { UserDto user = (id); return (user); } @PostMapping public ResponseEntity<UserDto> createUser(@RequestBody @Valid UserCreateRequest request) { UserDto createdUser = (request); return ().body(createdUser); } }
Writing MockMvc unit tests
import static .*; import static .*; import static .*; @ExtendWith() public class UserControllerUnitTest { @Mock private UserService userService; @InjectMocks private UserController userController; private MockMvc mockMvc; private ObjectMapper objectMapper; @BeforeEach void setUp() { // Set up MockMvc instance mockMvc = MockMvcBuilders .standaloneSetup(userController) .setControllerAdvice(new GlobalExceptionHandler()) // Add global exception handling .build(); objectMapper = new ObjectMapper(); } @Test void getUserById_ShouldReturnUser() throws Exception { // Prepare test data UserDto mockUser = new UserDto(1L, "John Doe", "john@"); // Configure Mock behavior when((1L)).thenReturn(mockUser); // Perform tests (get("/api/users/1") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.name").value("John Doe")) .andExpect(jsonPath("$.email").value("john@")); // Verify interaction verify(userService, times(1)).findById(1L); } @Test void createUser_ShouldReturnCreatedUser() throws Exception { // Prepare test data UserCreateRequest request = new UserCreateRequest("Jane Doe", "jane@"); UserDto createdUser = new UserDto(2L, "Jane Doe", "jane@"); // Configure Mock behavior when((any())).thenReturn(createdUser); // Perform tests (post("/api/users") .contentType(MediaType.APPLICATION_JSON) .content((request))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").value(2)) .andExpect(jsonPath("$.name").value("Jane Doe")) .andExpect(jsonPath("$.email").value("jane@")); // Verify interaction verify(userService, times(1)).createUser(any()); } @Test void getUserById_WhenUserNotFound_ShouldReturnNotFound() throws Exception { // Configure Mock behavior when((99L)).thenThrow(new UserNotFoundException("User not found")); // Perform tests (get("/api/users/99") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()); // Verify interaction verify(userService, times(1)).findById(99L); } }
Advantages and limitations
advantage
- Fast running: No need to start Spring context or embedded server
- Good isolation: only test the controller itself, no other components involved
- Accurately control dependency behavior: simulate service layer behavior through tools such as Mockito
- Easy to cover boundary conditions and exception paths
limitation
- Not testing Spring configuration and dependency injection mechanism
- Not verifying the correctness of the request mapped annotation
- Do not test filters, interceptors, and other web components
- May not reflect the complete behavior of the actual runtime
Solution 2: Use @WebMvcTest for slice testing
How it works
@WebMvcTest is a slice test annotation in Spring Boot tests. It only loads MVC-related components (controller, filter, WebMvcConfigurer, etc.) and will not start the complete application context.
This approach strikes a balance between unit testing and integration testing, both testing the correctness of Spring MVC configuration and avoiding full Spring context loading costs.
Implementation steps
Introduce dependencies
Same as scenario one, use spring-boot-starter-test dependency.
Write slice tests
import static .*; import static .*; import static .*; @WebMvcTest() public class UserControllerWebMvcTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Autowired private ObjectMapper objectMapper; @Test void getUserById_ShouldReturnUser() throws Exception { // Prepare test data UserDto mockUser = new UserDto(1L, "John Doe", "john@"); // Configure Mock behavior when((1L)).thenReturn(mockUser); // Perform tests (get("/api/users/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.name").value("John Doe")) .andExpect(jsonPath("$.email").value("john@")); } @Test void createUser_WithValidationError_ShouldReturnBadRequest() throws Exception { // Prepare invalid request data (required fields are missing) UserCreateRequest invalidRequest = new UserCreateRequest("", null); // Perform tests (post("/api/users") .contentType(MediaType.APPLICATION_JSON) .content((invalidRequest))) .andExpect(status().isBadRequest()) .andDo(print()); // Print request and response details for easy debugging } @Test void testSecurityConfiguration() throws Exception { // Test the endpoints that require certification (delete("/api/users/1")) .andExpect(status().isUnauthorized()); } }
Test custom filters and interceptors
@WebMvcTest() @Import({, }) public class UserControllerWithFiltersTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @MockBean private AuditService auditService; @Test void requestShouldPassThroughFiltersAndInterceptors() throws Exception { // Prepare test data UserDto mockUser = new UserDto(1L, "John Doe", "john@"); when((1L)).thenReturn(mockUser); // Execute the request and verify that the data is successfully returned after passing the filter and interceptor. (get("/api/users/1") .header("X-Trace-Id", "test-trace-id")) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(1)); // Verify that the interceptor calls the audit service verify(auditService, times(1)).logAccess(anyString(), eq("GET"), eq("/api/users/1")); } }
Advantages and limitations
advantage
- Test the integrity of MVC configuration: including request mapping, data binding, verification, etc.
- Covering filters and interceptors: Verify the entire MVC request processing link
- Faster startup speed: only load MVC related components, not complete application context
- Support test security configuration: Access control and authentication mechanisms can be verified
limitation
- Not testing the actual service implementation: Relying on the simulated service layer
- No test data access layer: no actual database interaction is involved
- Increased configuration complexity: more dependencies need to be simulated or excluded
- Although the startup speed is faster than the full integration test, it is slower than the pure unit test
Solution 3: Integration test based on @SpringBootTest
How it works
@SpringBootTest loads the full Spring application context, which can be integrated with embedded servers to test real HTTP requests and responses. This approach provides the closest test experience to the production environment, but is slow to start and is suitable for end-to-end functional verification.
Implementation steps
Introduce dependencies
<dependency> <groupId></groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Optional:If you need to test the database layer --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>test</scope> </dependency>
Writing integration tests (using mock ports)
@SpringBootTest @AutoConfigureMockMvc class UserControllerIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @Autowired private UserRepository userRepository; @BeforeEach void setUp() { (); // Prepare test data User user = new User(); (1L); ("John Doe"); ("john@"); (user); } @Test void getUserById_ShouldReturnUser() throws Exception { (get("/api/users/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.name").value("John Doe")) .andExpect(jsonPath("$.email").value("john@")); } @Test void createUser_ShouldSaveToDatabase() throws Exception { UserCreateRequest request = new UserCreateRequest("Jane Doe", "jane@"); (post("/api/users") .contentType(MediaType.APPLICATION_JSON) .content((request))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.name").value("Jane Doe")); // Verify that the data is actually saved to the database Optional<User> savedUser = ("jane@"); assertTrue(()); assertEquals("Jane Doe", ().getName()); } }
Writing integration tests (using real ports)
@SpringBootTest(webEnvironment = .RANDOM_PORT) class UserControllerServerIntegrationTest { @Autowired private TestRestTemplate restTemplate; @Autowired private UserRepository userRepository; @BeforeEach void setUp() { (); // Prepare test data User user = new User(); (1L); ("John Doe"); ("john@"); (user); } @Test void getUserById_ShouldReturnUser() { ResponseEntity<UserDto> response = ("/api/users/1", ); assertEquals(, ()); assertEquals("John Doe", ().getName()); } @Test void createUser_ShouldReturnCreatedUser() { UserCreateRequest request = new UserCreateRequest("Jane Doe", "jane@"); ResponseEntity<UserDto> response = ( "/api/users", request, ); assertEquals(, ()); assertNotNull(().getId()); assertEquals("Jane Doe", ().getName()); } @Test void testCaching() { // First request long startTime = (); ResponseEntity<UserDto> response1 = ("/api/users/1", ); long firstRequestTime = () - startTime; // The second request (should be retrieved from the cache) startTime = (); ResponseEntity<UserDto> response2 = ("/api/users/1", ); long secondRequestTime = () - startTime; // Verify that the same data is returned by two requests assertEquals(().getId(), ().getId()); // Usually cache requests are significantly faster than the first request assertTrue(secondRequestTime < firstRequestTime, "The second request should be faster (cache takes effect)"); } }
Overwrite production configuration using test configuration
Create a test-specific configuration filesrc/test/resources/
:
spring: datasource: url: jdbc:h2:mem:testdb driver-class-name: org. username: sa password: jpa: database-platform: .H2Dialect hibernate: ddl-auto: create-drop # Disable certain production environment componentsapp: scheduling: enabled: false external-services: payment-gateway: mock
Specify the configuration file in the test class:
@SpringBootTest(webEnvironment = .RANDOM_PORT) @ActiveProfiles("test") class UserControllerConfiguredTest { // Test content}
Advantages and limitations
advantage
- Comprehensive testing: Overwrite the complete process from HTTP requests to database
- Real behavior verification: test actual service implementation and component interaction
- Discover integration problems: Can identify problems when component integration
- Suitable for functional testing: Verify complete business functions
limitation
- Slow startup speed: need to load the full Spring context
- Poor isolation of tests: Tests may affect each other
- Complex configuration and setup: Need to manage the test environment configuration
- Debugging difficulty: complicated positioning problems when errors occur
- Not suitable for covering all scenarios: it is impossible to cover all boundary situations
Solution 4: Use TestRestTemplate/WebTestClient for end-to-end testing
How it works
This method uses an HTTP client designed for testing to send requests to the actually running embedded server, receive and verify the response. TestRestTemplate is suitable for synchronous testing, while WebTestClient supports testing for reactive and non-reactive applications and provides a smoother API.
Implementation steps
Use TestRestTemplate (synchronous test)
@SpringBootTest(webEnvironment = .RANDOM_PORT) class UserControllerE2ETest { @Autowired private TestRestTemplate restTemplate; @Test void testCompleteUserLifecycle() { // 1. Create a user UserCreateRequest createRequest = new UserCreateRequest("Test User", "test@"); ResponseEntity<UserDto> createResponse = ( "/api/users", createRequest, ); assertEquals(, ()); Long userId = ().getId(); // 2. Obtain the user ResponseEntity<UserDto> getResponse = ( "/api/users/" + userId, ); assertEquals(, ()); assertEquals("Test User", ().getName()); // 3. Update the user UserUpdateRequest updateRequest = new UserUpdateRequest("Updated User", null); ("/api/users/" + userId, updateRequest); // Verification update is successful ResponseEntity<UserDto> afterUpdateResponse = ( "/api/users/" + userId, ); assertEquals("Updated User", ().getName()); assertEquals("test@", ().getEmail()); // 4. Delete the user ("/api/users/" + userId); // Verify that the deletion is successful ResponseEntity<UserDto> afterDeleteResponse = ( "/api/users/" + userId, ); assertEquals(HttpStatus.NOT_FOUND, ()); } }
Using WebTestClient (supports reactive testing)
@SpringBootTest(webEnvironment = .RANDOM_PORT) class UserControllerWebClientTest { @Autowired private WebTestClient webTestClient; @Test void testUserApi() { // Create a user and get the ID UserCreateRequest createRequest = new UserCreateRequest("Reactive User", "reactive@"); UserDto createdUser = () .uri("/api/users") .contentType(MediaType.APPLICATION_JSON) .bodyValue(createRequest) .exchange() .expectStatus().isCreated() .expectBody() .returnResult() .getResponseBody(); Long userId = (); // Get the user () .uri("/api/users/{id}", userId) .exchange() .expectStatus().isOk() .expectBody() .jsonPath("$.name").isEqualTo("Reactive User") .jsonPath("$.email").isEqualTo("reactive@"); // Verification query API () .uri(uriBuilder -> uriBuilder .path("/api/users") .queryParam("email", "reactive@") .build()) .exchange() .expectStatus().isOk() .expectBodyList() .hasSize(1) .contains(createdUser); } @Test void testPerformance() { // Test API response time () .uri("/api/users") .exchange() .expectStatus().isOk() .expectBody() .consumeWith(response -> { long responseTime = () .getFirst("X-Response-Time") != null ? (().getFirst("X-Response-Time")) : 0; // Verify that the response time is within an acceptable range assertTrue(responseTime < 500, "API response time should be less than 500ms"); }); } }
Advantages and limitations
advantage
- Complete test: Verify the behavior of the application in a real environment
- End-to-end verification: Test the entire process from HTTP requests to database
- Comply with user perspective: Verify function from client perspective
- Support advanced scenarios: test authentication, performance, traffic, etc.
limitation
- Slow running: Full context startup time takes
- Environment dependencies: external services and resources may be required
- High maintenance costs: increased testing complexity and vulnerability
- Not suitable for unit coverage: It is difficult to cover all boundary situations
- Debugging difficulty: problem location and repair complexity
Solution comparison and selection suggestions
characteristic | MockMvc unit test | @WebMvcTest slice test | @SpringBootTest Integration Test | TestRestTemplate/WebTestClient |
---|---|---|---|---|
Context Loading | Not loading | Load only MVC components | Full load | Full load |
Start the server | no | no | Optional | yes |
Test speed | Fastest | quick | slow | The slowest |
Test isolation | Highest | high | middle | Low |
Coverage | Controller logic | MVC configuration and components | Full stack integration | Full stack end to end |
Configuration complexity | Low | middle | high | high |
Applicable scenarios | Controller unit logic | MVC configuration verification | Functional integration test | User-side experience verification |
Simulate dependencies | Completely simulated | Partial simulation | Small or non-simulated | Small or non-simulated |
Summarize
SpringBoot provides a wealth of tools and strategies for WebMVC testing, from lightweight unit testing to comprehensive end-to-end testing. Choosing the right test plan requires weighing test coverage, execution efficiency, maintenance costs, and team familiarity.
No matter which testing solution you choose, continuous testing and continuous improvement are the core concepts of software quality assurance.
This is the end of this article about the 4 methods of testing WebMVC in SpringBoot. For more related content on SpringBoot testing WebMVC, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!