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.
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
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.
Use this skill when:
<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>
dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.mockito:mockito-core")
}
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);
}
}
@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));
}
@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);
}
@Test
void shouldReturn400WhenRequestBodyInvalid() throws Exception {
mockMvc.perform(post("/api/users")
.contentType("application/json")
.content("{\"name\":\"\"}")) // Empty name
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors").isArray());
}
@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));
}
@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());
}
@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");
}
@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));
}
@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")));
}
@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());
}
@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"));
}
@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());
}
MockMvcBuilders.standaloneSetup()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.