You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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只能存在一个的限制,需要解决两个问题:

  1. 如何通过限定符区分并引用不同的Mock/Spy Bean,且能在对应测试方法中设置预期行为?
  2. 如何通过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冲突问题。

具体操作:

  • 先注入原始的SendMoneyUseCase Bean作为基准
  • 每个测试方法中,用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.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 01:20:25