
本文详解 spring boot 测试中因误用 @mockbean 导致服务层方法未实际执行的问题,指出控制器测试与服务层测试的关键区别,并提供可立即落地的修复方案与最佳实践。
本文详解 spring boot 测试中因误用 @mockbean 导致服务层方法未实际执行的问题,指出控制器测试与服务层测试的关键区别,并提供可立即落地的修复方案与最佳实践。
在 Spring Boot 的分层测试实践中,一个高频陷阱是:测试看似通过,但目标服务方法根本未被执行。如题所示,测试代码成功触发了 CategoryController.findById(),却未进入 CategoryService.findById() 方法体——其根本原因在于对 @MockBean 的误用。
? 问题定位:@MockBean 阻断了真实调用链
在原始测试类中:
@MockBean private CategoryService categoryService; // ❌ 关键错误
@MockBean 会完全替换 Spring 容器中的 CategoryService Bean,注入一个由 Mockito 创建的空模拟对象。这意味着:
- categoryController 依赖的 categoryService 是 mock 实例;
- 其所有方法默认返回 null(或空集合/默认值),不会委托给真实实现;
- 即使你在 @Spy 中配置了 CategoryResponseMapperImpl,也无法挽救服务层逻辑的缺失。
因此,控制器中 categoryService.findById(id) 调用返回 null,最终导致响应体 data 字段为空 —— 这并非映射器问题,而是服务层根本未参与执行。
✅ 正确解法:按测试目标选择依赖策略
✅ 场景一:测试控制器逻辑(推荐)
目标是验证 Controller 的 HTTP 行为、参数绑定、异常处理、响应格式等,不关心 Service 内部实现。此时应:
- 保留 @MockBean CategoryService;
- 显式 stub 其行为,确保返回预期 DTO。
@SpringBootTest(classes = {CategoryController.class, CategoryResponseMapperImpl.class})
class CategoryControllerTest {
@Autowired
private CategoryController categoryController;
@MockBean
private CategoryRepository categoryRepository; // 仍需 mock Repository(若 Service 未被加载)
@MockBean
private CategoryService categoryService; // ✅ 正确:mock Service
@Test
void test_findById_controller() throws Exception {
// 构造预期响应 DTO
CategoryResponse expectedDto = new CategoryResponse();
expectedDto.setId(123L);
expectedDto.setName("Category");
expectedDto.setOrdinal(1);
// Stub Service 方法(关键!)
when(categoryService.findById(123L)).thenReturn(expectedDto);
MockMvcBuilders.standaloneSetup(categoryController)
.build()
.perform(get("/api/v1/categories/{id}", 123L))
.andExpect(status().isOk())
.andExpect(content().json(
"""
{"timestamp":%d,"message":"Success","data":{"id":123,"name":"Category","ordinal":1}}
""".formatted(System.currentTimeMillis())));
}
}
⚠️ 注意:若 CategoryService 依赖 CategoryRepository,而你又未加载 CategoryService 的真实 Bean,则 @MockBean CategoryRepository 在此场景下可省略(因其未被调用)。但为清晰起见,建议仅 mock 直接依赖项。
✅ 场景二:测试服务层逻辑(推荐)
目标是验证 CategoryService.findById() 的业务逻辑(如数据库查询、MapStruct 映射、异常抛出等),此时应:
- 使用 @Autowired 注入真实 CategoryService;
- 仅 @MockBean 其直接依赖(如 CategoryRepository);
- 移除对 CategoryService 的 @MockBean。
@SpringBootTest(classes = {CategoryService.class, CategoryResponseMapperImpl.class})
class CategoryServiceTest {
@Autowired
private CategoryService categoryService;
@MockBean
private CategoryRepository categoryRepository;
@Test
void test_findById_service() {
// 准备测试数据
Category category = new Category();
category.setId(123L);
category.setName("Category");
category.setOrdinal(1);
when(categoryRepository.findById(123L)).thenReturn(Optional.of(category));
// 真实调用服务方法(✅ 进入方法体)
CategoryResponse result = categoryService.findById(123L);
// 断言映射结果
assertThat(result.getId()).isEqualTo(123L);
assertThat(result.getName()).isEqualTo("Category");
assertThat(result.getOrdinal()).isEqualTo(1);
}
}
? 关键原则总结
| 项目 | 控制器测试(@WebMvcTest 或 @SpringBootTest + standaloneSetup) | 服务层测试(@SpringBootTest 或 @ExtendWith(MockitoExtension.class)) |
|---|---|---|
| 被测对象 | @Controller | @Service |
| 目标 Bean | @Autowired Controller | @Autowired Service |
| 依赖 Bean | @MockBean 所有下游(Service、Repository 等) | @MockBean 仅直接依赖(如 Repository) |
| 是否执行 Service 方法体 | ❌ 否(除非手动 when(...).thenCallRealMethod(),不推荐) | ✅ 是(真实逻辑执行) |
? 最佳实践建议
-
优先使用切面化测试注解:
- Controller 测试 → @WebMvcTest(CategoryController.class)(自动扫描 Web 层,更轻量);
- Service 测试 → @SpringBootTest(classes = {...}) 或结合 @ContextConfiguration 精准加载。
- 避免混合测试目标:不要在一个测试类中既 mock 又 autowire 同一 Bean(如原文中 @MockBean CategoryService 与期望其执行逻辑矛盾)。
- 启用 @SpyBean 谨慎使用:仅当需部分模拟 + 部分真实调用时使用(如 spy mapper 的某方法),但需明确 @SpyBean 仍会创建代理,非无条件穿透。
遵循以上策略,即可精准控制测试边界,确保每次测试真正验证其设计目标,告别“方法未命中”的隐性故障。










