关于 unit-test-controller-layer
面向 Spring MVC 开发者的 MockMvc 控制器隔离测试技能,使用模拟服务依赖验证请求、响应、校验、响应头、内容协商与错误处理。
主要功能
- 01独立配置 MockMvc 控制器测试
- 02模拟服务层依赖
- 03验证 HTTP 方法与状态码
- 04检查 JSON 和响应正文
- 05测试校验、响应头与错误场景
- 06测试内容协商
适用场景
- 01测试 REST 控制器端点
- 02检查请求与响应处理
- 03验证校验结果和错误响应
- 04测试响应头与内容类型
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ giuseppe-trisciuoglio/ developer-kit --skill unit-test-controller-layer安装后在 Claude Code 中输入/unit-test-controller-layer确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/advanced.mdcommon-pitfalls.mdtroubleshooting.md详细介绍
使用 MockMvc 对 REST 控制器进行单元测试
概述
介绍使用 MockMvc 对 @RestController 和 @Controller 类进行单元测试的模式。涵盖请求与响应处理、HTTP 状态码、请求参数绑定、验证、内容协商、响应标头,以及使用模拟的服务依赖项处理异常。
适用场景
适用于:控制器测试、API 端点测试、Spring MVC 测试、模拟 HTTP 请求、Web 层端点单元测试、独立验证 REST 控制器。
说明
- 设置独立的 MockMvc:使用
MockMvcBuilders.standaloneSetup(controller)进行独立测试 - 模拟服务依赖项:使用
@Mock模拟所有服务,使用@InjectMocks注入控制器 - 测试 HTTP 方法:使用正确的状态码测试 GET、POST、PUT、PATCH、DELETE
- 验证响应:使用 JsonPath 断言 JSON,使用内容匹配器验证正文
- 测试验证:发送无效输入,验证返回 400 状态码及错误详情
- 测试错误:针对相应条件验证 404、400、401、403、500
- 验证标头:包括请求标头(Authorization)和响应标头
- 测试内容协商:使用不同的 Accept 和 Content-Type 标头
验证工作流
Run test → If fails: add .andDo(print()) → Check actual vs expected → Fix assertion
示例
Maven / Gradle 依赖项
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
基本模式:GET 端点
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;
@BeforeEach
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("$[0].id").value(1))
.andExpect(jsonPath("$[0].name").value("Alice"));
verify(userService, times(1)).getAllUsers();
}
@Test
void shouldReturn404WhenUserNotFound() throws Exception {
when(userService.getUserById(999L))
.thenThrow(new UserNotFoundException("User not found"));
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
verify(userService).getUserById(999L);
}
}
POST:创建资源
@Test
void shouldCreateUserAndReturn201() throws Exception {
UserDto createdUser = new UserDto(1L, "Alice", "[email protected]");
when(userService.createUser(any())).thenReturn(createdUser);
mockMvc.perform(post("/api/users")
.contentType("application/json")
.content("{\"name\":\"Alice\",\"email\":\"[email protected]\"}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("Alice"));
verify(userService).createUser(any(UserCreateRequest.class));
}
PUT:更新资源
@Test
void shouldUpdateUserAndReturn200() throws Exception {
UserDto updatedUser = new UserDto(1L, "Updated");
when(userService.updateUser(eq(1L), any())).thenReturn(updatedUser);
mockMvc.perform(put("/api/users/1")
.contentType("application/json")
.content("{\"name\":\"Updated\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Updated"));
verify(userService).updateUser(eq(1L), any());
}
DELETE:删除资源
@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 shouldFilterUsersByName() throws Exception {
when(userService.searchUsers("Alice")).thenReturn(List.of(new UserDto(1L, "Alice")));
mockMvc.perform(get("/api/users/search").param("name", "Alice"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").value("Alice"));
verify(userService).searchUsers("Alice");
}
路径变量
@Test
void shouldGetUserByIdFromPath() throws Exception {
when(userService.getUserById(123L)).thenReturn(new UserDto(123L, "Alice"));
mockMvc.perform(get("/api/users/{id}", 123L))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(123));
}
验证错误(400)
@Test
void shouldReturn400WhenRequestBodyInvalid() throws Exception {
mockMvc.perform(post("/api/users")
.contentType("application/json")
.content("{\"name\":\"\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors").isArray());
}
响应标头
@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"));
}
Authorization 标头
@Test
void shouldRequireAuthorizationHeader() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isUnauthorized());
mockMvc.perform(get("/api/users").header("Authorization", "Bearer token"))
.andExpect(status().isOk());
}
内容协商
@Test
void shouldReturnJsonWhenAcceptHeaderIsJson() throws Exception {
when(userService.getUserById(1L)).thenReturn(new UserDto(1L, "Alice"));
mockMvc.perform(get("/api/users/1").accept("application/json"))
.andExpect(status().isOk())
.andExpect(content().contentType("application/json"));
}
最佳实践
- 使用
standaloneSetup()独立测试控制器 - 模拟服务层——控制器处理 HTTP,服务处理业务逻辑
- 验证模拟对象的交互:
verify(service).method(args) - 测试正常流程和错误场景(404、400、500)
- 使用
jsonPath()进行流畅的 JSON 断言 - 每个测试方法只进行一项有针对性的断言
限制和警告
- 控制器测试只验证 HTTP 处理,不验证完整请求流程
- 不使用完整上下文时,
standaloneSetup()可能不支持@Validated - JsonPath 要求响应正文中包含有效的 JSON
@PreAuthorize/@Secured需要额外设置——可考虑单独进行安全测试- 文件上传需要
MockMultipartFile
参考资料
评论
支持 Markdown