Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 45

Testing REST Controllers with MockMvc and @SpringBootTest

Recommended resources

Hello! Welcome back to our series on testing in Spring.

In our last lesson, we focused on the persistence layer, using @DataJpaTest to write integration tests for our repositories. We learned how to leverage an in-memory database to verify our custom queries in a focused, isolated "slice" of the application.

Today, we're moving up the application stack to the web layer. This lesson directly addresses the learning outcome: Write an integration test for a REST controller using @SpringBootTest and MockMvc. We'll learn how to simulate HTTP requests to our @RestController endpoints and assert that they behave as expected, without needing to run a full web server. This is a critical skill for building robust and reliable REST APIs.

1. The Goal of Controller Integration Testing

When testing a REST controller, we aren't re-testing the full business logic of the service layer or the database queries—we've already covered those with unit and data-layer tests. Instead, our focus is on the responsibilities of the web layer itself:

  • Request Routing: Is the endpoint correctly mapped to the right URL and HTTP method (e.g., GET /api/products/{id})?
  • Data Binding: Are request parameters (@RequestParam), path variables (@PathVariable), and request bodies (@RequestBody) correctly bound to method arguments?
  • Serialization/Deserialization: Is the application correctly converting incoming JSON to Java objects and outgoing Java objects to JSON?
  • HTTP Response: Is the controller returning the correct HTTP status code (e.g., 200 OK, 404 Not Found) and headers?
  • Input Validation: Are validation annotations triggering correctly?

To achieve this, we will use MockMvc, a powerful tool from the spring-test module that lets us test our controllers in a mock servlet environment.


2. Testing Controllers: @WebMvcTest vs. @SpringBootTest

There are two primary ways to set up controller tests in Spring, each with different scopes and trade-offs. It's important to understand both.

  1. @WebMvcTest (Slice Test): Like @DataJpaTest, this is a "slice test" that loads only the web layer of your application (controllers, filters, JSON converters, etc.). It does not load your service or repository beans. It's fast and focused, ideal for testing a controller in isolation.
  2. @SpringBootTest + @AutoConfigureMockMvc (Full Context Test): This approach loads the entire Spring application context, just like when you run your application normally. It then provides a MockMvc instance to interact with it. This is slower but allows you to test the controller's integration with the complete application stack.

This lesson focuses on the second approach, as per the learning outcome. However, understanding @WebMvcTest provides valuable context.

The following video gives an excellent overview of different testing scopes, starting from a simple unit test, moving to a @WebMvcTest (which the author calls an "integration test"), and finally a @SpringBootTest (which he calls an "acceptance test"). This progression clearly illustrates the increasing scope and cost of each test type.

Spring Boot Testing a REST Controller with Unit, Integration & Acceptance Tests

Watch this video by Kris Foster to see a practical demonstration of building up tests from a simple unit test to a full-context integration test. This will help you understand the different layers of testing.

Pay close attention to the three distinct test classes created: Unit Test (07:56 - 10:44): A plain JUnit test that manually instantiates the controller and mocks its dependency. This is the most isolated form of testing. Integration Test (10:44 - 20:58): This section demonstrates a test using @WebMvcTest. Notice how it loads only the web layer and requires the service to be mocked with @MockBean. Acceptance Test (20:58 - 25:39): This final section demonstrates a test using @SpringBootTest and @AutoConfigureMockMvc. This is the core focus of our lesson. Observe that it loads the full context, but discovers a configuration error (the service wasn't annotated with @Service) that the @WebMvcTest missed because it was mocking the service.


3. Full-Context Integration Testing with @SpringBootTest and MockMvc

As you saw in the video, using @SpringBootTest provides a much higher-fidelity test. It ensures that your controller works correctly within the fully assembled application, including all its components, filters, and configuration.

Here’s the setup:

  1. @SpringBootTest: Tells Spring Boot to load the complete application context.
  2. @AutoConfigureMockMvc: Adds a MockMvc bean to the context, configured to call your web endpoints.
  3. @Autowired private MockMvc mockMvc;: Injects the MockMvc instance for you to use in your tests.

This diagram illustrates the architecture. We are testing the whole application context, but our test communicates with it through a mock servlet request rather than a real HTTP call over the network.

MockMVC - With Context
This diagram shows how a test using `@SpringBootTest` and `MockMvc` loads the full Application Context but does not start a web server. The test interacts with the controller via `MockMvc`, and dependencies can optionally be replaced with mocks.

Mocking Dependencies with @MockBean

Even when loading the full context, it's often desirable to replace certain beans with mocks. For example, you might want to avoid making real database calls or contacting external APIs. This keeps your controller test fast and deterministic.

The @MockBean annotation replaces any existing bean of the same type in the application context with a Mockito mock.

Let's look at a concrete example. Assume we have a ProductController that depends on a ProductService.

// ProductController.java
@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<ProductDTO> getProductById(@PathVariable Long id) {
        ProductDTO product = productService.findProductById(id);
        return ResponseEntity.ok(product);
    }
}

Here is how we would write an integration test for it using @SpringBootTest.

// ProductControllerIntegrationTest.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

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

@SpringBootTest
@AutoConfigureMockMvc
class ProductControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc; // Injects the MockMvc instance

    @MockBean
    private ProductService productService; // Replaces the real ProductService with a mock

    @Test
    void getProductById_whenProductExists_shouldReturnProductDTO() throws Exception {
        // Arrange
        long productId = 1L;
        ProductDTO productDTO = new ProductDTO(productId, "Test Laptop", 1500.00);
        when(productService.findProductById(productId)).thenReturn(productDTO);

        // Act & Assert
        mockMvc.perform(get("/api/products/{id}", productId))
                .andExpect(status().isOk())
                .andExpect(content().contentType("application/json"))
                .andExpect(jsonPath("$.id", is(1)))
                .andExpect(jsonPath("$.name", is("Test Laptop")));
    }
    
    // Test for a 404 Not Found scenario would go here...
}

To learn more about this powerful combination of annotations, the following article provides a clear, code-driven explanation.

Spring Boot Integration Testing With @SpringBootTest

The article 'Spring Boot Integration Testing With @SpringBootTest' from Code With Arho explains how to set up these tests and how to use @MockBean to isolate your application from external services.

Read the sections 'Write an Integration Test With a Mock Environment' and 'Mock an External Service With @MockBean'. The first section shows the exact annotation setup we are using, and the second demonstrates using @MockBean, which is conceptually identical to how we are mocking our service layer.


4. Choosing the Right Approach

Given your background in system design, you'll appreciate the trade-offs. So, when should you use @WebMvcTest versus @SpringBootTest?

  • Use @WebMvcTest for focused controller tests. If your goal is to quickly verify mappings, serialization, and validation in isolation, this is the best choice. It's much faster, especially in large applications.
  • Use @SpringBootTest + @AutoConfigureMockMvc for broader integration tests. If you need to verify that your controller works correctly with other components like Spring Security filters, global exception handlers (@ControllerAdvice), or other framework features, this approach is necessary.

The article below provides excellent recommendations on when to use each approach and also discusses the hybrid method we are focusing on.

Choosing Between MockMvc and @SpringBootTest for Controller Testing

The article 'Choosing Between MockMvc and @SpringBootTest for Controller Testing' by Philip Rieck offers a great summary of the pros, cons, and ideal use cases for each testing strategy.

Read the sections: 'Testing Spring MVC Controller with MockMvc' to understand the benefits of the slice test approach. 'Recommendation: When to Use Which Testing Approach' for a concise summary of the trade-offs. 'Combining Both MockMvc and @SpringBootTest' which describes the exact hybrid approach we've implemented in this lesson.


Conclusion

In this lesson, we bridged the gap between isolated unit tests and full-blown application tests. You've learned how to write powerful integration tests for the web layer that validate your REST controllers within a complete or sliced application context.

Key Takeaways:

  • MockMvc is the primary tool for testing Spring controllers without starting a real web server. It simulates requests and allows for detailed assertions on the response.
  • @WebMvcTest provides a fast, lightweight "slice test" that only loads the web layer components. It's excellent for testing controllers in isolation.
  • @SpringBootTest with @AutoConfigureMockMvc loads the entire application context, providing a high-fidelity environment to test how your controller integrates with the rest of your application.
  • @MockBean is a crucial annotation that allows you to replace any bean in the application context with a mock, enabling you to isolate your tests from databases, external services, or complex business logic.

In our next lesson, we will take a deeper look at @MockBean. While we used it today to facilitate our controller test, the next session will be dedicated to understanding its behavior, its impact on the Spring test context cache, and how to use it effectively in various integration testing scenarios.