mcpField
U
unit-test-controller-layer

unit-test-controller-layer

github.com/giuseppe-trisciuoglio测试与质量3653.8k 次安装7 周前更新
代码仓库

关于 unit-test-controller-layer

面向 Spring MVC 开发者的 MockMvc 控制器隔离测试技能,使用模拟服务依赖验证请求、响应、校验、响应头、内容协商与错误处理。

主要功能

  1. 01独立配置 MockMvc 控制器测试
  2. 02模拟服务层依赖
  3. 03验证 HTTP 方法与状态码
  4. 04检查 JSON 和响应正文
  5. 05测试校验、响应头与错误场景
  6. 06测试内容协商

适用场景

  1. 01测试 REST 控制器端点
  2. 02检查请求与响应处理
  3. 03验证校验结果和错误响应
  4. 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.md
common-pitfalls.md
troubleshooting.md

详细介绍

使用 MockMvc 对 REST 控制器进行单元测试

概述

介绍使用 MockMvc 对 @RestController 和 @Controller 类进行单元测试的模式。涵盖请求与响应处理、HTTP 状态码、请求参数绑定、验证、内容协商、响应标头,以及使用模拟的服务依赖项处理异常。

适用场景

适用于:控制器测试、API 端点测试、Spring MVC 测试、模拟 HTTP 请求、Web 层端点单元测试、独立验证 REST 控制器。

说明

  1. 设置独立的 MockMvc:使用 MockMvcBuilders.standaloneSetup(controller) 进行独立测试
  2. 模拟服务依赖项:使用 @Mock 模拟所有服务,使用 @InjectMocks 注入控制器
  3. 测试 HTTP 方法:使用正确的状态码测试 GET、POST、PUT、PATCH、DELETE
  4. 验证响应:使用 JsonPath 断言 JSON,使用内容匹配器验证正文
  5. 测试验证:发送无效输入,验证返回 400 状态码及错误详情
  6. 测试错误:针对相应条件验证 404、400、401、403、500
  7. 验证标头:包括请求标头(Authorization)和响应标头
  8. 测试内容协商:使用不同的 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