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.
/plugin marketplace add giuseppe-trisciuoglio/developer-kit/plugin install developer-kit@giuseppe.trisciuoglioThis 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.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.