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

为何@MockBean未替换目标Bean却生成了Mock实例?

@MockBean未替换真实Bean的问题

应用代码

MyListener.java

@Component
@AllArgsConstructor
@Slf4j
public class MyListener {

  private final AmqpTemplate amqpTemplate;
  private final MyService myService;

  @RabbitListener(queues = "q_request")
  public void listen(@Payload final String value) {
    log.info("Listener: {}, {}", myService.hashCode(), value);
    final String result = myService.get(value);
    amqpTemplate.convertAndSend("test", "response", result);
  }
}

MyService.java

@Service
@AllArgsConstructor
@Slf4j
public class MyService {

  public String get(final String value) {
    log.info("Service: {}, {} ", this.hashCode(), value);
    return "realService";
  }
}

测试代码

ApplicationTest.java

@SpringBootTest(webEnvironment = WebEnvironment.NONE)
class ApplicationTest {

  @Test
  void testContextLoads() {}
}

MyListenerTest.java

@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@Slf4j
class MyListenerTest {

  @Autowired private RabbitTemplate rabbitTemplate;
  @MockBean MyService myService;

  @TestConfiguration
  static class TestRabbitConfig {

    @Bean
    public Queue responseQueue() {
      return new Queue("q_response", false, false, false);
    }

    @Bean
    public Binding responseBinding(final DirectExchange directExchange, final Queue responseQueue) {
      return BindingBuilder.bind(responseQueue).to(directExchange).with("response");
    }
  }

  @Test
  void testMessaging() {

    log.info(
        "Test: {}, {}, {}",
        myService.hashCode(),
        mockingDetails(myService).isMock(),
        mockingDetails(myService).isSpy());

    when(myService.get("value")).thenReturn("result");

    rabbitTemplate.convertAndSend("test", "request", "value");

    final Message message = rabbitTemplate.receive("q_response", 10000);
    assertNotNull(message);
    verify(myService).get("value");
  }
}

日志信息

测试类中拿到的MyService是Mock实例,但实际执行的是真实Bean:

2024-08-08T11:01:31.861Z  INFO 105 --- [           main] test.MyListenerTest                      : Started MyListenerTest in 0.818 seconds (process running for 4.28)
2024-08-08T11:01:31.877Z  INFO 105 --- [           main] test.MyListenerTest                      : Test: 1719990258, true, false
2024-08-08T11:01:31.952Z  INFO 105 --- [ntContainer#0-1] test.MyListener                          : Listener: 63583816, value
2024-08-08T11:01:31.953Z  INFO 105 --- [ntContainer#0-1] test.MyService                           : Service: 63583816, value 
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.982 s <<< FAILURE! - in test.MyListenerTest
[ERROR] test.MyListenerTest.testMessaging  Time elapsed: 0.126 s  <<< FAILURE!
Wanted but not invoked:
myService bean.get("value");
-> at test.MyService.get(MyService.java:15)
Actually, there were zero interactions with this mock.
    at test.MyService.get(MyService.java:15)
    at test.MyListenerTest.testMessaging(MyListenerTest.java:62)

环境信息

  • Java 17
  • Spring Boot 3.1.9
  • Maven

已做排查

  • 测试过去两年正常,近期因Docker构建环境变化导致测试执行顺序变更,问题出现
  • 修改测试名称调整执行顺序后,所有测试可正常通过
  • 怀疑是Spring上下文缓存问题,但官方文档说明@MockBean会修改上下文缓存键,不应被复用
  • 官方文档明确@MockBean会替换同类型的现有Bean
  • 嵌套的@TestConfiguration不会覆盖主配置,不影响上下文缓存
  • 在ApplicationTest上添加@DirtiesContext可解决问题,但测试顺序变更后需调整;在MyListenerTest上添加无效

问题

为什么@MockBean创建了Mock实例,但没有替换MyListener中依赖的真实MyServiceBean?


解答

这是Spring上下文缓存与RabbitMQ容器初始化顺序冲突导致的问题:

  1. 上下文缓存复用异常:
    当ApplicationTest先执行时,会初始化一个不含@MockBean的纯净上下文并缓存。后续MyListenerTest执行时,理论上@MockBean会生成新的缓存键,但Spring在处理@TestConfiguration与@MockBean的组合时,可能错误复用了之前的纯净上下文。

  2. RabbitListener容器提前持有真实Bean引用:
    纯净上下文初始化时,MyListener的@RabbitListener会触发RabbitMQ消息容器启动,此时容器内部已经持有真实MyService的引用。即使后续MyListenerTest尝试替换MyService为Mock,已经启动的消息容器不会重新注入新Bean,依然使用之前的真实实例。

解决方法

方法1:强制隔离上下文(推荐)

在ApplicationTest上添加@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS),确保测试完成后销毁上下文,避免后续测试复用。或者给MyListenerTest添加@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS),强制在测试前创建全新上下文。

方法2:控制RabbitListener初始化时机

使用@RabbitListenerTest禁用RabbitListener自动启动,等MockBean注入完成后再手动启动容器:

@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@Slf4j
@RabbitListenerTest
class MyListenerTest {

  @Autowired private RabbitTemplate rabbitTemplate;
  @MockBean MyService myService;
  @Autowired private RabbitListenerEndpointRegistry registry;

  @BeforeEach
  void setUp() {
    // 启动所有RabbitListener容器
    registry.getListenerContainers().forEach(MessageListenerContainer::start);
  }

  @TestConfiguration
  static class TestConfig {
    @Bean
    public RabbitListenerTestHarness rabbitListenerTestHarness() {
      return new RabbitListenerTestHarness();
    }
  }

  // 测试方法保持不变
}

方法3:固定测试执行顺序

在pom.xml中配置Maven Surefire插件,确保MyListenerTest先于ApplicationTest执行:

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <configuration>
        <runOrder>alphabetical</runOrder>
      </configuration>
    </plugin>
  </plugins>
</build>

内容的提问来源于stack exchange,提问作者dur

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 23:19:52