在使用 MockMvc 进行 Spring Web 控制器单元测试时,若控制器方法抛出自定义异常(如 ResourceNotFoundException),但测试失败并显示 NestedServletException,通常是因为 @ControllerAdvice 异常处理器未被加载到 MockMvc 上下文中。
在使用 mockmvc 进行 spring web 控制器单元测试时,若控制器方法抛出自定义异常(如 `resourcenotfoundexception`),但测试失败并显示 `nestedservletexception`,通常是因为 `@controlleradvice` 异常处理器未被加载到 mockmvc 上下文中。
在基于 @WebMvcTest 的测试中,Spring Boot 默认会扫描并注册 @ControllerAdvice 组件(如 AppExceptionHandler)——但前提是未显式调用 MockMvcBuilders.standaloneSetup(...) 覆盖默认行为。而你的测试类中存在一个关键矛盾:
@WebMvcTest(useDefaultFilters = false) // ← 禁用了自动组件扫描
// ...
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(underTest).build(); // ← 手动构建,完全绕过 Spring 容器
}
standaloneSetup() 创建的是“纯手工” MockMvc 实例,它不会自动注册任何 @ControllerAdvice、@ExceptionHandler 或消息转换器(如 Jackson),即使你已声明 @WebMvcTest。因此,当控制器抛出 ResourceNotFoundException 时,异常无法被 AppExceptionHandler 捕获并转换为 HTTP 404 响应,而是直接穿透为 NestedServletException,导致测试崩溃。
✅ 正确做法是:二选一,避免混合使用。
✅ 方案一:使用 @WebMvcTest 自动配置(推荐)
移除手动 standaloneSetup(),让 Spring Boot 自动装配完整的 MVC 环境(含 @ControllerAdvice、ObjectMapper、HandlerExceptionResolver 等):
@WebMvcTest(AppUserController.class) // 明确指定被测控制器,无需 useDefaultFilters = false
class AppUserControllerTest {
@Autowired
private MockMvc mockMvc; // 直接注入,由 @WebMvcTest 提供
@MockBean // 注意:不是 @Mock,而是 @MockBean,确保注入到 Spring 上下文
private UserDetailsServiceImpl userDetailsService;
@MockBean
private AppUserMapper appUserMapper;
@Autowired
private ObjectMapper objectMapper; // Spring Boot 自动配置的 ObjectMapper
@Test
void findAppUserById_throwsNotFound() throws Exception {
UUID uuid = UUID.fromString("ac0a1859-8e59-4ede-8def-043c32aa3208");
// 模拟服务层抛出异常
when(userDetailsService.findAppUserById(any(UUID.class)))
.thenThrow(new ResourceNotFoundException("Kullanıcı bulunamadı"));
// 断言响应状态码与响应体
mockMvc.perform(get("/app-users/{id}", uuid))
.andExpect(status().isNotFound())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.httpStatusCode").value(404))
.andExpect(jsonPath("$.message").value("Kullanıcı bulunamadı"));
}
}
? 关键修正点:
- 使用 @MockBean 替代 @Mock,确保模拟对象被注册进 Spring Test Context;
- 删除 setUp() 中的手动 standaloneSetup(),改用 @Autowired MockMvc;
- 移除 useDefaultFilters = false,或至少保留 @WebMvcTest(AppUserController.class) 显式限定目标;
- 利用 jsonPath() 断言 JSON 响应字段(更健壮,无需手动反序列化)。
✅ 方案二:坚持 standaloneSetup()?需显式注册所有依赖
仅在特殊场景(如隔离性极强的白盒测试)下才选用。此时必须手动添加异常处理器和消息转换器:
@BeforeEach
void setUp() {
this.mockMvc = MockMvcBuilders.standaloneSetup(underTest)
.setControllerAdvice(new AppExceptionHandler()) // 必须显式注册
.setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper)) // 注册 JSON 转换器
.build();
}
⚠️ 注意:此方式下 @MockBean 失效,需用 @Mock + @InjectMocks 手动组装,且 ObjectMapper 也需自行实例化,维护成本高,不推荐用于常规 REST API 异常测试。
? 额外建议:优化异常设计(可选)
当前 ApplicationException 继承 RuntimeException 并通过 @ExceptionHandler 统一处理,是合理实践。但注意:
- 确保 AppExceptionHandler 类被 @WebMvcTest 扫描到(默认会,除非 useDefaultFilters = false 且未显式 @Import);
- @JsonIgnoreProperties 已排除敏感字段,符合安全规范;
- @ControllerAdvice 无 basePackages 限制时,对全局生效。
✅ 总结
| 问题根源 | standaloneSetup() 覆盖了 @WebMvcTest 的自动配置,导致 @ControllerAdvice 未生效 |
|---|---|
| 核心修复 | 弃用手动 setup,改用 @Autowired MockMvc + @MockBean,信任 Spring Boot Test 的约定优于配置 |
| 最佳实践 | 使用 jsonPath() 断言响应结构;用 @MockBean 替代 @Mock;避免 useDefaultFilters = false 除非明确需要 |
遵循上述调整后,ResourceNotFoundException 将被 AppExceptionHandler 正确捕获,返回标准 JSON 错误响应,测试即可稳定通过。











