Spring Boot Test中如何在同一测试类混用@MockBean与@SpyBean?
问题描述
现有Spring Boot Test测试类如下:
@SpringBootTest @AutoConfigureMockMvc class SendMoneyControllerSpringBootSpyBeanTest { @Autowired private MockMvc mockMvc; @SpyBean private SendMoneyUseCase sendMoneyUseCase; @Test void testSendMoneyWithMock() { ... } @Test void testSendMoneyWithSpy() { ... } }
需要在两个测试方法中分别使用SendMoneyUseCase的Mock版本和Spy版本,但直接同时声明@MockBean和@SpyBean会触发Spring容器同类型Bean只能存在一个的限制,需要解决两个问题:
- 如何通过限定符区分并引用不同的Mock/Spy Bean,且能在对应测试方法中设置预期行为?
- 如何通过Spring Boot API编程式动态创建Mock Bean,替代注解方式实现不同测试方法的Bean切换?
解决方案
方案一:用@Qualifier+@Primary区分Mock/Spy Bean
Spring允许同类型存在多个Bean,通过@Qualifier指定名称引用,再配合@Primary指定默认生效的Bean,就能在不同测试里灵活切换。
具体操作:
- 给
@MockBean和@SpyBean设置明确的名称,其中一个标记为@Primary(业务代码默认注入该Bean) - 在测试类中通过
@Qualifier注入两个Bean实例 - 不同测试方法中,手动替换容器内的Primary Bean,让控制器使用对应的Mock/Spy实例,再设置预期行为
代码示例:
@SpringBootTest @AutoConfigureMockMvc class SendMoneyControllerSpringBootSpyBeanTest { @Autowired private MockMvc mockMvc; @Autowired private ConfigurableApplicationContext context; // 声明Mock Bean,指定名称,不设为Primary @MockBean(name = "sendMoneyUseCaseMock") private SendMoneyUseCase sendMoneyUseCaseMock; // 声明Spy Bean,指定名称并设为Primary(业务代码默认注入) @Primary @SpyBean(name = "sendMoneyUseCaseSpy") private SendMoneyUseCase sendMoneyUseCaseSpy; @Test void testSendMoneyWithMock() { // 1. 切换容器中的Primary Bean为Mock版本 DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getAutowireCapableBeanFactory(); beanFactory.registerSingleton("sendMoneyUseCase", sendMoneyUseCaseMock); // 2. 设置Mock的预期行为 Mockito.when(sendMoneyUseCaseMock.sendMoney(anyLong(), anyLong(), anyBigDecimal())) .thenReturn(true); // 3. 执行测试请求 mockMvc.perform(post("/send-money") .contentType(MediaType.APPLICATION_JSON) .content("{\"sourceAccountId\":1,\"targetAccountId\":2,\"amount\":100}")) .andExpect(status().isOk()); } @Test void testSendMoneyWithSpy() { // 1. 切换容器中的Primary Bean为Spy版本 DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getAutowireCapableBeanFactory(); beanFactory.registerSingleton("sendMoneyUseCase", sendMoneyUseCaseSpy); // 2. 设置Spy的Stub行为(部分方法模拟) Mockito.doReturn(true).when(sendMoneyUseCaseSpy).sendMoney(anyLong(), anyLong(), anyBigDecimal()); // 3. 执行测试请求 mockMvc.perform(post("/send-money") .contentType(MediaType.APPLICATION_JSON) .content("{\"sourceAccountId\":1,\"targetAccountId\":2,\"amount\":100}")) .andExpect(status().isOk()); // 4. 验证Spy的方法调用次数 Mockito.verify(sendMoneyUseCaseSpy, times(1)).sendMoney(1L, 2L, BigDecimal.valueOf(100)); } }
注意:如果业务代码是通过@Autowired直接注入SendMoneyUseCase,必须切换Primary Bean才能让控制器使用对应的Mock/Spy;如果业务代码支持通过@Qualifier指定名称,也可以直接修改控制器的依赖字段。
方案二:编程式动态创建Mock/Spy Bean(无注解方式)
通过Spring Boot的BeanFactory API,在每个测试方法执行前动态注册Mock或Spy实例,测试后恢复原Bean,彻底避免多Bean冲突问题。
具体操作:
- 先注入原始的
SendMoneyUseCaseBean作为基准 - 每个测试方法中,用Mockito创建Mock/Spy实例,注册到容器中替换原Bean
- 用
@BeforeEach在每次测试前恢复原始Bean,保证测试隔离性
代码示例:
@SpringBootTest @AutoConfigureMockMvc class SendMoneyControllerSpringBootSpyBeanTest { @Autowired private MockMvc mockMvc; @Autowired private ConfigurableApplicationContext context; @Autowired private SendMoneyUseCase originalSendMoneyUseCase; private SendMoneyUseCase currentBean; @BeforeEach void resetBean() { // 每次测试前重置为原始Bean,保证测试独立 DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getAutowireCapableBeanFactory(); beanFactory.registerSingleton("sendMoneyUseCase", originalSendMoneyUseCase); } @Test void testSendMoneyWithMock() { // 1. 创建Mock实例 currentBean = Mockito.mock(SendMoneyUseCase.class); // 2. 注册到容器替换原Bean DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getAutowireCapableBeanFactory(); beanFactory.registerSingleton("sendMoneyUseCase", currentBean); // 3. 设置Mock预期行为 Mockito.when(currentBean.sendMoney(anyLong(), anyLong(), anyBigDecimal())) .thenReturn(true); // 4. 执行测试 mockMvc.perform(post("/send-money") .contentType(MediaType.APPLICATION_JSON) .content("{\"sourceAccountId\":1,\"targetAccountId\":2,\"amount\":100}")) .andExpect(status().isOk()); } @Test void testSendMoneyWithSpy() { // 1. 基于原始Bean创建Spy实例 currentBean = Mockito.spy(originalSendMoneyUseCase); // 2. 注册到容器替换原Bean DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getAutowireCapableBeanFactory(); beanFactory.registerSingleton("sendMoneyUseCase", currentBean); // 3. 设置Spy的Stub行为 Mockito.doReturn(true).when(currentBean).sendMoney(anyLong(), anyLong(), anyBigDecimal()); // 4. 执行测试 mockMvc.perform(post("/send-money") .contentType(MediaType.APPLICATION_JSON) .content("{\"sourceAccountId\":1,\"targetAccountId\":2,\"amount\":100}")) .andExpect(status().isOk()); // 5. 验证Spy的方法调用 Mockito.verify(currentBean, times(1)).sendMoney(1L, 2L, BigDecimal.valueOf(100)); } }
关键说明:
- 用
@BeforeEach重置Bean,确保每个测试都从原始状态开始,不会互相干扰 - 通过
registerSingleton()动态替换容器中的Bean实例,完全控制Mock/Spy的生命周期 - 直接用Mockito创建实例,无需依赖
@MockBean/@SpyBean注解,灵活性更高
内容的提问来源于stack exchange,提问作者François F.
相关产品推荐
相关产品推荐

