为何@MockBean未替换目标Bean却生成了Mock实例?
应用代码
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容器初始化顺序冲突导致的问题:
上下文缓存复用异常:
当ApplicationTest先执行时,会初始化一个不含@MockBean的纯净上下文并缓存。后续MyListenerTest执行时,理论上@MockBean会生成新的缓存键,但Spring在处理@TestConfiguration与@MockBean的组合时,可能错误复用了之前的纯净上下文。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

