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

如何在Spring ApplicationReadyEvent触发前Stub方法?测试时机问题

解决Spring Boot测试中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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 01:53:11