mcpField
U
unit-test-exception-handler

unit-test-exception-handler

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

关于 unit-test-exception-handler

面向 Spring Boot 开发者,提供使用 MockMvc 对 `@ControllerAdvice` 和 `@ExceptionHandler` 编写单元测试的模式,验证 HTTP 状态、JSON 错误响应及字段级校验错误。

主要功能

  1. 01通过 MockMvc 隔离测试
  2. 02验证 HTTP 状态码
  3. 03检查 JSON 错误字段
  4. 04测试字段级校验错误
  5. 05模拟处理器依赖

适用场景

  1. 01测试全局异常处理
  2. 02验证 REST API 错误格式
  3. 03检查自定义错误载荷
  4. 04测试无效请求的校验响应

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-exception-handler

安装后在 Claude Code 中输入/unit-test-exception-handler确认已加载。

附带文件references/examples.md

详细介绍

Unit Testing ExceptionHandler and ControllerAdvice

概述

此技能介绍了编写 Spring Boot 异常处理器单元测试的模式。内容涵盖使用 MockMvc 测试 @ControllerAdvice 类中的 @ExceptionHandler 方法,包括 HTTP 状态断言、JSON 响应验证、字段级验证错误测试,以及模拟处理器依赖项。

适用场景

  • 为 @ExceptionHandler 方法编写单元测试
  • 测试 @ControllerAdvice 全局异常处理
  • 验证 REST API 错误响应格式
  • 在控制器测试中模拟异常
  • 测试字段级验证错误响应
  • 断言自定义错误负载和 HTTP 状态代码

说明

  1. 创建测试控制器,使其抛出特定异常,以触发各个 @ExceptionHandler
  2. 通过 MockMvcBuilders.standaloneSetup() 上的 setControllerAdvice() 注册 ControllerAdvice
  3. 使用 .andExpect(status().isXxx()) 断言 HTTP 状态代码
  4. 使用 jsonPath("$.field") 匹配器验证错误响应字段
  5. 测试验证错误:发送无效负载,并检查 MethodArgumentNotValidException 是否生成字段级详细信息
  6. 调试失败:使用 .andDo(print());如果处理器未被调用,请确认已调用 setControllerAdvice(),且异常类型匹配

示例

异常处理器和错误 DTO
@ControllerAdvice
public class GlobalExceptionHandler {

  @ExceptionHandler(ResourceNotFoundException.class)
  @ResponseStatus(HttpStatus.NOT_FOUND)
  public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
    return new ErrorResponse(404, "Not Found", ex.getMessage());
  }

  @ExceptionHandler(ValidationException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public ErrorResponse handleValidation(ValidationException ex) {
    return new ErrorResponse(400, "Bad Request", ex.getMessage());
  }

  @ExceptionHandler(MethodArgumentNotValidException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public ValidationErrorResponse handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
    Map<String, String> errors = new HashMap<>();
    ex.getBindingResult().getFieldErrors().forEach(e -> errors.put(e.getField(), e.getDefaultMessage()));
    return new ValidationErrorResponse(400, "Validation Failed", errors);
  }
}

public record ErrorResponse(int status, String error, String message) {}
public record ValidationErrorResponse(int status, String error, Map<String, String> errors) {}
单元测试
@ExtendWith(MockitoExtension.class)
class GlobalExceptionHandlerTest {

  private MockMvc mockMvc;

  @BeforeEach
  void setUp() {
    GlobalExceptionHandler handler = new GlobalExceptionHandler();
    mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
        .setControllerAdvice(handler)
        .build();
  }

  @Test
  void shouldReturn404WhenResourceNotFound() throws Exception {
    mockMvc.perform(get("/api/users/999"))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.status").value(404))
        .andExpect(jsonPath("$.error").value("Not Found"))
        .andExpect(jsonPath("$.message").value("User not found"));
  }

  @Test
  void shouldReturn400WithFieldErrorsOnValidationFailure() throws Exception {
    mockMvc.perform(post("/api/users")
        .contentType("application/json")
        .content("{\"name\":\"\",\"email\":\"invalid\"}"))
        .andExpect(status().isBadRequest())
        .andExpect(jsonPath("$.status").value(400))
        .andExpect(jsonPath("$.errors.name").value("must not be blank"))
        .andExpect(jsonPath("$.errors.email").value("must be a valid email"));
  }
}

@RestController
@RequestMapping("/api")
class TestController {
  @GetMapping("/users/{id}") public User getUser(@PathVariable Long id) {
    throw new ResourceNotFoundException("User not found");
  }
  @PostMapping("/users") public User createUser(@RequestBody @Valid User user) {
    throw new ValidationException("Validation failed");
  }
}

最佳实践

  • 使用专门抛出的异常,分别测试每个 @ExceptionHandler 方法
  • 通过 setControllerAdvice() 仅注册一个 @ControllerAdvice 实例,切勿遗漏
  • 断言错误响应正文中的所有字段,而不只是 HTTP 状态
  • 对于验证错误,同时验证字段名称键和错误消息值
  • 使用 MockMvcBuilders.standaloneSetup() 隔离测试处理器,无需完整的 Spring 上下文
  • 记录断言失败:串联 .andDo(print()),以便测试失败时打印请求和响应

常见问题

  • 未调用处理器:确保在构建器上调用了 setControllerAdvice()
  • JsonPath 不匹配:使用 .andDo(print()) 检查实际响应结构
  • 状态为 200:处理器方法缺少 @ResponseStatus
  • 处理器重复:@Order 控制优先级;更具体的异常类型优先
  • 测试处理器逻辑而非行为:模拟外部依赖项,只测试响应转换

限制和警告

  • @ExceptionHandler 匹配特异性:优先匹配更具体的异常类型;Exception.class 会捕获所有未匹配的类型
  • @ResponseStatus 默认值:未使用 @ResponseStatus 或返回 ResponseEntity 时,HTTP 状态默认为 200
  • 全局与局部范围:@ControllerAdvice 中的 @ExceptionHandler 是全局的;在控制器中声明时,仅对该控制器生效
  • 日志副作用:对于会记录日志的处理器,应使用 verify(mockLogger).logXxx(...) 进行验证
  • 本地化:使用 MessageSource 时,使用不同的 Locale 值进行测试,以确认消息解析结果
  • 安全上下文:AuthorizationException 处理器可以访问 SecurityContextHolder——请测试上下文是否得到正确评估

评论

支持 Markdown