本文详解 spring boot 集成测试中因误用 @mockbean 导致服务层方法未执行的根本原因,并提供控制器测试与服务层测试两种场景下的正确配置方式、代码示例及关键注意事项。
本文详解 spring boot 集成测试中因误用 @mockbean 导致服务层方法未执行的根本原因,并提供控制器测试与服务层测试两种场景下的正确配置方式、代码示例及关键注意事项。
在 Spring Boot 的分层测试实践中,一个常见却易被忽视的问题是:控制器测试看似通过,但实际并未触发真实的服务逻辑(如 categoryService.findById()),返回数据为 null 或空响应。根本原因在于测试配置中对 CategoryService 的错误处理——你使用了 @MockBean,这会完全替换 Spring 容器中的 CategoryService Bean 为一个空的 Mockito Mock 实例,其所有方法默认返回 null(或空集合、默认值),导致 categoryService.findById(id) 永远不会执行原始业务逻辑。
✅ 正确做法取决于你的测试目标
场景一:你想测试控制器行为(Controller Test)
此时应 Mock 服务层(CategoryService),让控制器逻辑可验证,而无需依赖真实服务实现。你需要显式定义 categoryService.findById() 的模拟返回值:
@SpringBootTest(classes = {CategoryController.class, CategoryResponseMapperImpl.class})
class CategoryControllerTest {
@Autowired
private CategoryController categoryController;
@MockBean
private CategoryRepository categoryRepository; // 仍需 Mock Repository(若 Controller 不直接依赖)
@MockBean
private CategoryService categoryService; // ✅ 正确:Mock Service 层
@SpyBean // 推荐替代 @Spy + 手动构造,确保被 Spring 管理
private CategoryResponseMapper categoryResponseMapper;
@Test
void test_findById_controller() throws Exception {
// 准备 DTO 响应(非 Entity!)
CategoryResponse mockResponse = new CategoryResponse();
mockResponse.setId(123L);
mockResponse.setName("Category");
mockResponse.setOrdinal(1);
// ✅ 关键:必须 stub service 方法返回值
when(categoryService.findById(eq(123L))).thenReturn(mockResponse);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(categoryController).build();
mockMvc.perform(get("/api/v1/categories/{id}", 123L))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.data.id").value(123L))
.andExpect(jsonPath("$.data.name").value("Category"));
}
}⚠️ 注意:@SpyBean 比 @Spy 更安全,它将 Spy 注入 Spring 上下文并参与 AOP,避免手动 new 实例导致的 Mapper 未被 Spring 管理问题;同时 @SpyBean 要求 CategoryResponseMapperImpl 必须有无参构造或 Spring 可实例化的构造方式。
场景二:你想测试服务层逻辑(Service Test)
此时应 注入真实 CategoryService,仅 Mock 其依赖(如 CategoryRepository),确保 findById() 方法真正执行:
@SpringBootTest(classes = {CategoryService.class, CategoryResponseMapperImpl.class})
class CategoryServiceTest {
@Autowired
private CategoryService categoryService; // ✅ 注入真实 Service 实例
@MockBean
private CategoryRepository categoryRepository; // ✅ Mock 其直接依赖
@SpyBean
private CategoryResponseMapper categoryResponseMapper;
@Test
void test_findById_service() {
// 构建测试用 Category Entity
Category category = Category.builder()
.id(123L)
.name("Category")
.ordinal(1)
.build();
// Stub Repository 返回 Optional.of(category)
when(categoryRepository.findById(eq(123L))).thenReturn(Optional.of(category));
// ✅ 真实调用 service 方法
CategoryResponse result = categoryService.findById(123L);
// 断言结果(验证 MapStruct 是否正确转换)
assertThat(result.getId()).isEqualTo(123L);
assertThat(result.getName()).isEqualTo("Category");
assertThat(result.getOrdinal()).isEqualTo(1);
}
}? 核心总结与最佳实践
- @MockBean ≠ @Autowired:@MockBean 会替换容器中同类型的 Bean,使其所有方法返回默认值(非 null 的类型如 int 返回 0,Collection 返回空集合)。若未显式 when(...).thenReturn(...),调用即“静默失败”。
- 明确测试边界:控制器测试关注 HTTP 协议、状态码、JSON 结构,应 Mock 下游服务;服务测试关注业务规则、数据转换、异常流程,应 Mock 数据访问层。
- 慎用 @SpyBean for Mapper:MapStruct 实现类(如 CategoryResponseMapperImpl)通常无状态且轻量,用 @SpyBean 可验证 toDto() 是否被调用;若仅需功能,@Autowired 亦可(前提是已通过 @Mapper(componentModel = "spring") 启用 Spring 支持)。
- 避免混合配置:不要在同一个测试类中既 @MockBean CategoryService 又期望它执行真实逻辑——这是逻辑矛盾。
遵循以上原则,即可精准控制测试粒度,确保每一层逻辑都得到可靠验证。

















