如何在Spring ApplicationReadyEvent触发前Stub方法?测试时机问题
业务代码中,ManagementService通过@EventListener(ApplicationReadyEvent.class)在应用启动时执行temp.print(),测试时使用@SpyBean和@BeforeEach来Stubtemp.print(),但首次测试时该方法仍会执行——因为ApplicationContext初始化时事件监听器已触发,而@BeforeEach的Stub是在这之后才执行的,后续测试因为上下文缓存复用了已Stub的Bean,所以正常。需求是在上下文初始化前完成Stub,同时保留Temp其他方法的真实行为(不想用@MockBean全Mock)。
方案一:利用BeanPostProcessor在Bean初始化后立即Stub
通过自定义BeanPostProcessor,在Temp的SpyBean初始化完成后立刻Stub目标方法,确保在ApplicationReadyEvent触发前完成Stub。
修改测试基类,添加@TestConfiguration:
@SpringBootTest public abstract class SpringIntegrationBaseTest { @SpyBean protected ManagementService service; @SpyBean protected Temp temp; @TestConfiguration static class StubTempConfig { @Bean public BeanPostProcessor tempStubProcessor() { return new BeanPostProcessor() { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { // 当Bean是Temp类型时,立即Stub print方法 if (bean instanceof Temp) { Temp tempSpy = (Temp) bean; Mockito.doNothing().when(tempSpy).print(); } return bean; } }; } } }
原理
BeanPostProcessor的postProcessAfterInitialization会在每个Bean初始化完成后执行,而ApplicationReadyEvent是在所有Bean初始化完成、上下文刷新完毕后才发布的。因此,Temp的SpyBean初始化后会被立即Stub,后续ManagementService的startup方法触发时,调用的就是已经Stub好的print()方法。
方案二:手动构建ApplicationContext并提前注入Stub好的Spy
如果需要更灵活的控制,可以手动创建ApplicationContext,提前SpyTemp并注册到上下文,确保上下文初始化时就使用已Stub的Bean:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) public abstract class SpringIntegrationBaseTest { protected ManagementService service; protected Temp temp; @BeforeEach public void setUp() { // 提前Spy Temp并Stub目标方法 temp = Mockito.spy(new Temp()); Mockito.doNothing().when(temp).print(); // 手动启动Spring上下文,注册Stub好的Temp ApplicationContext context = new SpringApplicationBuilder(YourApplicationClass.class) .bean(Temp.class, () -> temp) .run(); service = context.getBean(ManagementService.class); } }
注意
这种方法每次测试都会重新启动上下文,会增加测试耗时,适合不需要上下文缓存的场景。
方案三:在业务代码中添加测试环境判断(可选)
如果允许修改业务代码,可以给startup方法添加条件,测试环境下跳过执行:
@EventListener(ApplicationReadyEvent.class) public void startup(@Value("${spring.profiles.active:}") String activeProfile) { if ("test".equals(activeProfile)) { log.info("Skip startup logic in test environment"); return; } log.info("Startup on application ready."); temp.print(); }
然后测试时激活test profile:
@SpringBootTest(properties = "spring.profiles.active=test") public abstract class SpringIntegrationBaseTest { // ... }
这种方法简单直接,但侵入了业务代码,适合对代码侵入容忍度较高的场景。
内容的提问来源于stack exchange,提问作者pebble unit

