关于 unit-test-exception-handler
面向 Spring Boot 开发者,提供使用 MockMvc 对 `@ControllerAdvice` 和 `@ExceptionHandler` 编写单元测试的模式,验证 HTTP 状态、JSON 错误响应及字段级校验错误。
主要功能
- 01通过 MockMvc 隔离测试
- 02验证 HTTP 状态码
- 03检查 JSON 错误字段
- 04测试字段级校验错误
- 05模拟处理器依赖
适用场景
- 01测试全局异常处理
- 02验证 REST API 错误格式
- 03检查自定义错误载荷
- 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 状态代码
说明
- 创建测试控制器,使其抛出特定异常,以触发各个
@ExceptionHandler - 通过
MockMvcBuilders.standaloneSetup()上的setControllerAdvice()注册 ControllerAdvice - 使用
.andExpect(status().isXxx())断言 HTTP 状态代码 - 使用
jsonPath("$.field")匹配器验证错误响应字段 - 测试验证错误:发送无效负载,并检查
MethodArgumentNotValidException是否生成字段级详细信息 - 调试失败:使用
.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