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).

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.
- Arrange: Use Mockito's
when(...).thenReturn(...)to stub the behavior of your@MockBeanservice. - Act: Use
mockMvc.perform(...)to execute an HTTP request. - 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.$.idrefers to theidfield 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.
- Create a new test class named
UserControllerTestin the same package structure undersrc/test/java. - Annotate the class with
@WebMvcTest(UserController.class). - Inject
MockMvcusing@Autowiredand yourUserServiceusing@MockBean. - Write a test for getting all users:
- Name it
whenGetAllUsers_thenReturns200AndUserList. - Arrange: Create a
Listcontaining twoUserobjects. Stub theuserService.getAllUsers()method to return this list. - Act & Assert: Perform a
GETrequest 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.
- Expect the status to be
- Name it
- 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 theuserService.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: ForMockMvcto test this properly, your controller should catch the exception and return aResponseEntitywith a 404 status. - Act & Assert: Perform a
GETrequest to/api/users/999.- Expect the status to be
404 Not Found.
- Expect the status to be
- Name it
After writing your tests, run them in your IDE. You should see a successful output similar to this, confirming your controller logic is sound.

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.
@WebMvcTestcreates a "test slice" that loads only the web layer, leading to fast and focused controller tests.@MockBeanis used to add mock objects to the SpringApplicationContext, replacing real dependencies like services.MockMvcis the primary tool for simulating HTTP requests (perform) and asserting on their responses (andExpect).jsonPathis 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.