SoFunction
Updated on 2025-04-23

4 implementation solutions for SpringBoot testing WebMVC

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

&lt;dependency&gt;
    &lt;groupId&gt;&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
    &lt;scope&gt;test&lt;/scope&gt;
&lt;/dependency&gt;
&lt;!-- Optional:If you need to test the database layer --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.h2database&lt;/groupId&gt;
    &lt;artifactId&gt;h2&lt;/artifactId&gt;
    &lt;scope&gt;test&lt;/scope&gt;
&lt;/dependency&gt;

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&lt;User&gt; 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&lt;UserDto&gt; response = ("/api/users/1", );
        
        assertEquals(, ());
        assertEquals("John Doe", ().getName());
    }
    
    @Test
    void createUser_ShouldReturnCreatedUser() {
        UserCreateRequest request = new UserCreateRequest("Jane Doe", "jane@");
        
        ResponseEntity&lt;UserDto&gt; response = (
                "/api/users", request, );
                
        assertEquals(, ());
        assertNotNull(().getId());
        assertEquals("Jane Doe", ().getName());
    }
    
    @Test
    void testCaching() {
        // First request        long startTime = ();
        ResponseEntity&lt;UserDto&gt; response1 = ("/api/users/1", );
        long firstRequestTime = () - startTime;
        
        // The second request (should be retrieved from the cache)        startTime = ();
        ResponseEntity&lt;UserDto&gt; 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 &lt; 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&lt;UserDto&gt; createResponse = (
                "/api/users", createRequest, );
                
        assertEquals(, ());
        Long userId = ().getId();
        
        // 2. Obtain the user        ResponseEntity&lt;UserDto&gt; 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&lt;UserDto&gt; 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&lt;UserDto&gt; 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 -&gt; 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 -&gt; {
                    long responseTime = ()
                            .getFirst("X-Response-Time") != null
                            ? (().getFirst("X-Response-Time"))
                            : 0;
                            
                    // Verify that the response time is within an acceptable range                    assertTrue(responseTime &lt; 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!