--- name: unit-test-controller-layer description: Unit tests for REST controllers using MockMvc and @WebMvcTest. Test request/response mapping, validation, and exception handling. Use when testing web layer endpoints in isolation. category: testing tags: [junit-5, mockito, unit-testing, controller, rest, mockmvc] version: 1.0.1 --- # Unit Testing REST Controllers with MockMvc Test @RestController and @Controller classes by mocking service dependencies and verifying HTTP responses, status codes, and serialization. Use MockMvc for lightweight controller testing without loading the full Spring context. ## When to Use This Skill Use this skill when: - Testing REST controller request/response handling - Verifying HTTP status codes and response formats - Testing request parameter binding and validation - Mocking service layer for isolated controller tests - Testing content negotiation and response headers - Want fast controller tests without integration test overhead ## Setup: MockMvc + Mockito ### Maven ```xml org.springframework.boot spring-boot-starter-test test org.mockito mockito-core test ``` ### Gradle ```kotlin dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.mockito:mockito-core") } ``` ## Basic Pattern: Testing GET Endpoint ### Simple GET Endpoint Test ```java import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @ExtendWith(MockitoExtension.class) class UserControllerTest { @Mock private UserService userService; @InjectMocks private UserController userController; private MockMvc mockMvc; void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(userController).build(); } @Test void shouldReturnAllUsers() throws Exception { List users = List.of( new UserDto(1L, "Alice"), new UserDto(2L, "Bob") ); when(userService.getAllUsers()).thenReturn(users); mockMvc.perform(get("/api/users")) .andExpect(status().isOk()) .andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$[0].id").value(1)) .andExpect(jsonPath("$[0].name").value("Alice")) .andExpect(jsonPath("$[1].id").value(2)); verify(userService, times(1)).getAllUsers(); } @Test void shouldReturnUserById() throws Exception { UserDto user = new UserDto(1L, "Alice"); when(userService.getUserById(1L)).thenReturn(user); mockMvc.perform(get("/api/users/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.name").value("Alice")); verify(userService).getUserById(1L); } } ``` ## Testing POST Endpoint ### Create Resource with Request Body ```java @Test void shouldCreateUserAndReturn201() throws Exception { UserCreateRequest request = new UserCreateRequest("Alice", "alice@example.com"); UserDto createdUser = new UserDto(1L, "Alice", "alice@example.com"); when(userService.createUser(any(UserCreateRequest.class))) .thenReturn(createdUser); mockMvc.perform(post("/api/users") .contentType("application/json") .content("{\"name\":\"Alice\",\"email\":\"alice@example.com\"}")) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.name").value("Alice")) .andExpect(jsonPath("$.email").value("alice@example.com")); verify(userService).createUser(any(UserCreateRequest.class)); } ``` ## Testing Error Scenarios ### Handle 404 Not Found ```java @Test void shouldReturn404WhenUserNotFound() throws Exception { when(userService.getUserById(999L)) .thenThrow(new UserNotFoundException("User not found")); mockMvc.perform(get("/api/users/999")) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.error").value("User not found")); verify(userService).getUserById(999L); } ``` ### Handle 400 Bad Request ```java @Test void shouldReturn400WhenRequestBodyInvalid() throws Exception { mockMvc.perform(post("/api/users") .contentType("application/json") .content("{\"name\":\"\"}")) // Empty name .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.errors").isArray()); } ``` ## Testing PUT/PATCH Endpoints ### Update Resource ```java @Test void shouldUpdateUserAndReturn200() throws Exception { UserUpdateRequest request = new UserUpdateRequest("Alice Updated"); UserDto updatedUser = new UserDto(1L, "Alice Updated"); when(userService.updateUser(eq(1L), any(UserUpdateRequest.class))) .thenReturn(updatedUser); mockMvc.perform(put("/api/users/1") .contentType("application/json") .content("{\"name\":\"Alice Updated\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.name").value("Alice Updated")); verify(userService).updateUser(eq(1L), any(UserUpdateRequest.class)); } ``` ## Testing DELETE Endpoint ### Delete Resource ```java @Test void shouldDeleteUserAndReturn204() throws Exception { doNothing().when(userService).deleteUser(1L); mockMvc.perform(delete("/api/users/1")) .andExpect(status().isNoContent()); verify(userService).deleteUser(1L); } @Test void shouldReturn404WhenDeletingNonExistentUser() throws Exception { doThrow(new UserNotFoundException("User not found")) .when(userService).deleteUser(999L); mockMvc.perform(delete("/api/users/999")) .andExpect(status().isNotFound()); } ``` ## Testing Request Parameters ### Query Parameters ```java @Test void shouldFilterUsersByName() throws Exception { List users = List.of(new UserDto(1L, "Alice")); when(userService.searchUsers("Alice")).thenReturn(users); mockMvc.perform(get("/api/users/search?name=Alice")) .andExpect(status().isOk()) .andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$[0].name").value("Alice")); verify(userService).searchUsers("Alice"); } ``` ### Path Variables ```java @Test void shouldGetUserByIdFromPath() throws Exception { UserDto user = new UserDto(123L, "Alice"); when(userService.getUserById(123L)).thenReturn(user); mockMvc.perform(get("/api/users/{id}", 123L)) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(123)); } ``` ## Testing Response Headers ### Verify Response Headers ```java @Test void shouldReturnCustomHeaders() throws Exception { when(userService.getAllUsers()).thenReturn(List.of()); mockMvc.perform(get("/api/users")) .andExpect(status().isOk()) .andExpect(header().exists("X-Total-Count")) .andExpect(header().string("X-Total-Count", "0")) .andExpect(header().string("Content-Type", containsString("application/json"))); } ``` ## Testing Request Headers ### Send Request Headers ```java @Test void shouldRequireAuthorizationHeader() throws Exception { mockMvc.perform(get("/api/users")) .andExpect(status().isUnauthorized()); mockMvc.perform(get("/api/users") .header("Authorization", "Bearer token123")) .andExpect(status().isOk()); } ``` ## Content Negotiation ### Test Different Accept Headers ```java @Test void shouldReturnJsonWhenAcceptHeaderIsJson() throws Exception { UserDto user = new UserDto(1L, "Alice"); when(userService.getUserById(1L)).thenReturn(user); mockMvc.perform(get("/api/users/1") .accept("application/json")) .andExpect(status().isOk()) .andExpect(content().contentType("application/json")); } ``` ## Advanced: Testing Multiple Status Codes ```java @Test void shouldReturnDifferentStatusCodesForDifferentScenarios() throws Exception { // Successful response when(userService.getUserById(1L)).thenReturn(new UserDto(1L, "Alice")); mockMvc.perform(get("/api/users/1")) .andExpect(status().isOk()); // Not found when(userService.getUserById(999L)) .thenThrow(new UserNotFoundException("Not found")); mockMvc.perform(get("/api/users/999")) .andExpect(status().isNotFound()); // Unauthorized mockMvc.perform(get("/api/admin/users")) .andExpect(status().isUnauthorized()); } ``` ## Best Practices - **Use standalone setup** when testing single controller: `MockMvcBuilders.standaloneSetup()` - **Mock service layer** - controllers should focus on HTTP handling - **Test happy path and error paths** thoroughly - **Verify service method calls** to ensure controller delegates correctly - **Use content() matchers** for response body validation - **Keep tests focused** on one endpoint behavior per test - **Use JsonPath** for fluent JSON response assertions ## Common Pitfalls - **Testing business logic in controller**: Move to service tests - **Not mocking service layer**: Always mock service dependencies - **Testing framework behavior**: Focus on your code, not Spring code - **Hardcoding URLs**: Use MockMvcRequestBuilders helpers - **Not verifying mock interactions**: Always verify service was called correctly ## Troubleshooting **Content type mismatch**: Ensure `contentType()` matches controller's `@PostMapping(consumes=...)` or use default. **JsonPath not matching**: Use `mockMvc.perform(...).andDo(print())` to see actual response content. **Status code assertions fail**: Check controller `@RequestMapping`, `@PostMapping` status codes and error handling. ## References - [Spring MockMvc Documentation](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/web/servlet/MockMvc.html) - [JsonPath for REST Assertions](https://goessner.net/articles/JsonPath/) - [Spring Testing Best Practices](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing)