| 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
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
Gradle
dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.mockito:mockito-core")
}
Basic Pattern: Testing GET Endpoint
Simple GET Endpoint Test
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<UserDto> 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
@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
@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
@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
@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
@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
@Test
void shouldFilterUsersByName() throws Exception {
List<UserDto> 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
@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
@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
@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
@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
@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.