Skip to main content

Welcome to a Grasp lesson

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

Lesson 75

MockMvc for REST Controller Integration Testing

Recommended resources

Welcome back! In our previous lesson, you gained a deep understanding of unit testing with Mockito, learning how to isolate your service layer and verify its behavior with precision using techniques like verify() and ArgumentCaptor. You now have a robust way to ensure that individual components of your application logic work as intended.

Today, we're moving one level up in the testing hierarchy to integration testing. While unit tests check components in isolation, integration tests verify that they work together correctly. We will specifically focus on the web layer of your application. Your goal is to answer the question: "When a real HTTP request comes in, does my REST controller handle it correctly, call the right service method, and return the expected HTTP response?"

By the end of this lesson, you will be ableto write comprehensive integration tests for your REST controllers using Spring's MockMvc framework, allowing you to simulate HTTP requests and validate your API's behavior without needing to run a full web server.

From Unit Tests to Integration Tests

In the last two lessons, you focused on unit testing your UserService. This is what's often called a narrow test—it targets a very small, isolated slice of your application. But what happens when these slices are connected? For instance, how do we test that the annotations on your controller, like @GetMapping and @RequestBody, are configured correctly to route an HTTP request to the right method?

This is where integration tests come in. Let's first clarify the different scopes of testing.

Spring Boot Testing Mini-Series #6: Integration Tests

This video from the "Code With Arho" channel provides an excellent definition of narrow and broad integration tests.

Watch the segment from the beginning. It clearly explains the difference between testing a part of the application (narrow) versus the whole application (broad). This distinction is key to understanding the different testing strategies in Spring Boot.

For our purposes today, we will focus on a narrow integration test for the web layer. We want to test the controller and the Spring MVC infrastructure around it, but we will still mock the service layer beneath it. This gives us the best of both worlds: we can test the entire web request/response flow while keeping our tests fast and isolated from the database.


The Tool for the Job: MockMvc and @WebMvcTest

Spring Boot provides a fantastic testing slice annotation called @WebMvcTest. When you use this annotation on a test class, Spring Boot will only configure the web layer of your application (controllers, filters, JSON serializers, etc.). It will not load your service or repository beans. This makes it perfect for focused, fast integration tests of your controllers.

To interact with this web layer test slice, we use MockMvc. This powerful class allows you to "perform" mock HTTP requests against your controller and make assertions about the result.

The diagram below illustrates this concept perfectly. The test scope includes the controller, but it excludes the full application context and mocks the repository (and by extension, the service that uses it).

This diagram shows the scope of a `@WebMvcTest`. The test focuses on the controller and its interactions, while dependencies like repositories or services are replaced with mocks. The full application context and web server are not started, making the test faster.

Let's see how these pieces fit together in code.

Unit testing Spring MVC applications with JUnit 5

This InfoWorld article provides a great walkthrough of setting up a controller test with @WebMvcTest.

Read the section titled "Unit testing a Spring MVC controller with MockMvc". Pay close attention to the three key annotations it introduces: @WebMvcTest(WidgetController.class): Specifies which controller to load. @Autowired private MockMvc mockMvc;: Injects the main testing tool. @MockBean private WidgetService widgetService;: Replaces the real service with a Mockito mock in the application context. This is the integration test equivalent of the @Mock annotation you used in unit tests.


Writing Your First MockMvc Test

Let's translate the theory into practice. The process follows a familiar pattern: arrange, act, and assert.

  1. Arrange: Use Mockito's when(...).thenReturn(...) to stub the behavior of your @MockBean service.
  2. Act: Use mockMvc.perform(...) to execute an HTTP request.
  3. Assert: Chain .andExpect(...) calls to verify the HTTP status, headers, and response body.

This video from Kris Foster provides a very clear, hands-on demonstration of this entire process.

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

The instructor in this video does a great job of building an integration test from scratch, explaining each component as he goes.

Watch the segment from the start of the integration test. Focus on how he: Adds the @WebMvcTest and @MockBean annotations. Uses when() to define the behavior of the mocked service for both a default case and a custom case. Builds the request using mockMvc.perform(get(...)). Asserts the response status with .andExpect(status().isOk()). Asserts the response body with .andExpect(content().string(...)). Uses verify() on the mock bean to ensure the service method was called with the correct parameters.

Dissecting a MockMvc Test

Let's break down a typical test for a POST endpoint to create a user. For this, you'll need to send a JSON payload in the request body. You can use an ObjectMapper to convert your Java DTO into a JSON string.

@WebMvcTest(UserController.class)
public class UserControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper; // Spring Boot provides this in the test context

    @MockBean
    private UserService userService;

    @Test
    void whenCreateUser_withValidInput_thenReturns201() throws Exception {
        // Arrange
        UserDto userDto = new UserDto("test@example.com", "Test User");
        User createdUser = new User(1L, "test@example.com", "Test User");

        // Stub the service layer
        when(userService.createUser(any(UserDto.class))).thenReturn(createdUser);

        // Act & Assert
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(userDto)))
                .andExpect(status().isCreated()) // Assert HTTP 201 Created
                .andExpect(jsonPath("$.id", is(1))) // Assert response body using JSONPath
                .andExpect(jsonPath("$.email", is("test@example.com")));
    }
}

The most powerful part of this is jsonPath(). It allows you to query the JSON response using expressions to verify specific fields.

  • $ is the root of the JSON object.
  • $.id refers to the id field at the root.
  • $[0] would refer to the first element in a JSON array.

Full-Stack Integration Testing with @SpringBootTest

The @WebMvcTest approach is excellent for testing the web layer in isolation. However, sometimes you want to test the entire flow, from the controller all the way down to a database. This is a broad integration test.

For this, you can use the @SpringBootTest annotation instead. This will load your entire Spring application context. When combined with @AutoConfigureMockMvc, you can still use MockMvc to make requests, but now those requests will trigger the real service, the real repository, and interact with a real (usually in-memory) database like H2.

Integration Testing in Spring Boot with JUnit 5 & MockMvc

This article from Java Guides provides a complete example of a full-stack integration test using @SpringBootTest.

Skim through the following sections to see how this approach differs: Step 3: Notice the use of @SpringBootTest instead of @WebMvcTest. It also autowires the actual UserRepository. Step 4: The test for creating a user looks very similar, but there's no @MockBean and no when(...).thenReturn(...). It's testing the full stack. Step 5: Here, the test first saves a user directly via the repository (userRepository.save(user)) to set up the database state before performing the GET request. This approach is more comprehensive but also slower and more complex to set up. For now, we will focus on @WebMvcTest, but it's important to know this alternative exists for true end-to-end validation.


Your Turn: Test Your UserController

Now it's time to apply these concepts to your project.

  1. Create a new test class named UserControllerTest in the same package structure under src/test/java.
  2. Annotate the class with @WebMvcTest(UserController.class).
  3. Inject MockMvc using @Autowired and your UserService using @MockBean.
  4. Write a test for getting all users:
    • Name it whenGetAllUsers_thenReturns200AndUserList.
    • Arrange: Create a List containing two User objects. Stub the userService.getAllUsers() method to return this list.
    • Act & Assert: Perform a GET request to /api/users.
      • Expect the status to be 200 OK.
      • Use jsonPath("$.size()") to assert that the response array has a size of 2.
      • Use jsonPath("$[0].email") to assert the email of the first user in the list matches your test data.
  5. Write a test for a user not found scenario:
    • Name it whenGetUserById_withNonExistentId_thenReturns404.
    • Arrange: You'll need a custom exception, like UserNotFoundException, that your service throws. Stub the userService.getUserById(...) to throw this exception. Make sure your controller has an exception handler for this (or use @ControllerAdvice, which we'll cover later). For now, a basic service-level exception is fine. Note: For MockMvc to test this properly, your controller should catch the exception and return a ResponseEntity with a 404 status.
    • Act & Assert: Perform a GET request to /api/users/999.
      • Expect the status to be 404 Not Found.

After writing your tests, run them in your IDE. You should see a successful output similar to this, confirming your controller logic is sound.

An example of a passed integration test in IntelliJ IDEA. The green checkmark signifies that the simulated HTTP request to the `/hello/{name}` endpoint behaved exactly as expected.

Conclusion

In this lesson, you've bridged the gap between unit testing and full end-to-end testing by mastering integration tests for your Spring Boot controllers. You now have a reliable way to validate your API's contracts and request-handling logic.

Key Takeaways:

  • Integration tests verify that multiple application components work together correctly.
  • @WebMvcTest creates a "test slice" that loads only the web layer, leading to fast and focused controller tests.
  • @MockBean is used to add mock objects to the Spring ApplicationContext, replacing real dependencies like services.
  • MockMvc is the primary tool for simulating HTTP requests (perform) and asserting on their responses (andExpect).
  • jsonPath is an essential matcher for validating the structure and content of JSON response bodies.

You are now equipped to build and test robust REST APIs. In our next lesson, we will zoom out and discuss the architectural patterns that govern how different parts of a large application—or even different applications—communicate with each other. We will explore the key differences between monolithic and microservices architectures, setting the stage for building more complex, distributed systems.