返回

Spring应用启动前Stub方法的三种方案

java

在 Spring ApplicationReadyEvent 之前 Stub 方法的几种方案

在 Spring 应用启动过程中,有时需要在 ApplicationReadyEvent 事件触发之前对某些 Bean 的方法进行 Stub 操作,尤其是在测试场景下。这通常是因为需要在应用完全初始化之前模拟某些行为,避免真实调用带来的副作用或者依赖问题。下面介绍几种常见的解决方案。

问题分析

问题的核心在于 ApplicationReadyEvent 事件触发时, @BeforeEach 注解的方法还没有执行,导致 Stub 操作滞后。问题的执行流程如下:

应用上下文初始化 --> 事件监听器被调用 --> @BeforeEach 开始 Stub 方法 --> 测试方法执行

而期望的流程是:

??? --> Stubbing --> ??? --> 事件监听器被调用 --> ??? --> 测试方法执行

其中 ??? 代表任意中间步骤。

解决方案

方案一:使用 @TestConfigurationBeanPostProcessor

这种方案利用 Spring 的 BeanPostProcessor 机制,在 Bean 初始化完成后,但在 ApplicationReadyEvent 触发前对 Bean 进行修改。

原理:

  • @TestConfiguration 注解创建一个测试专用的配置类,它里面的 Bean 只在测试环境下有效。
  • BeanPostProcessor 接口提供两个回调方法,postProcessBeforeInitializationpostProcessAfterInitialization ,分别在 Bean 初始化前后进行操作。我们可以实现 postProcessAfterInitialization 方法,在 Bean 初始化后对其进行 Stub 操作。

步骤:

  1. 创建一个内部类,用 @TestConfiguration 注解标记,并实现 BeanPostProcessor 接口。
  2. postProcessAfterInitialization 方法中,判断当前 Bean 是否是需要 Stub 的目标 Bean,如果是,则使用 Mockito.doNothing().when() 进行 Stub。
  3. 将这个配置类注册到 Spring 容器中。

代码示例:

import org.mockito.Mockito;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.TestExecutionListeners;

@SpringBootTest
@TestExecutionListeners(listeners = {StubbingTestExecutionListener.class}, mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
public abstract class SpringIntegrationBaseTest {

    @SpyBean
    protected ManagementService service;

    @SpyBean
    protected Temp temp;

    @Configuration
    static class StubbingTestConfig {
        @Bean
        public BeanPostProcessor stubbingBeanPostProcessor() {
            return new BeanPostProcessor() {
                @Override
                public Object postProcessBeforeInitialization(Object bean, String beanName) {
                    return bean; // do nothing before initialization
                }

                @Override
                public Object postProcessAfterInitialization(Object bean, String beanName) {
                   if (bean instanceof Temp tempBean) {
                       Mockito.doNothing().when(tempBean).print();
                       System.out.println("Stubbing print() method of Temp bean");
                    }
                   return bean;
                }
            };
        }
    }

    static class StubbingTestExecutionListener implements TestExecutionListener {

        @Override
        public void beforeTestClass(TestContext testContext) {

        }

        @Override
        public void prepareTestInstance(TestContext testContext) {

        }

        @Override
        public void beforeTestMethod(TestContext testContext) {

        }

        @Override
        public void beforeTestExecution(TestContext testContext) {

        }

        @Override
        public void afterTestExecution(TestContext testContext){

        }

        @Override
        public void afterTestMethod(TestContext testContext){

        }

        @Override
        public void afterTestClass(TestContext testContext) {

        }
    }
}

这段代码里,我们通过定义StubbingTestConfig ,创建了一个静态内部配置类。其中,定义了一个BeanPostProcessor 的Bean,它会在每个Bean初始化之后进行检查,如果发现Bean是 Temp 类型的,就会对其print方法进行Stub。然后通过实现自定义的TestExecutionListenerStubbingTestExecutionListener,在beforeTestClass的回调里初始化 StubbingTestConfig 配置。
此外为了避免和其他TestExecutionListener的冲突,需要使用 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS

安全建议:

  • BeanPostProcessor 会影响到所有符合条件的 Bean,确保判断逻辑准确,避免误操作。
  • Stub 操作应该具有针对性,不要过度 Stub,以免影响其他测试用例或代码逻辑。

方案二: 使用 TestExecutionListener

利用 Spring Test 框架提供的 TestExecutionListener 接口,在测试类实例化后,但在事件监听器触发前执行 Stub 操作。

原理:

  • TestExecutionListener 接口定义了一系列回调方法,可以在测试执行的不同阶段执行自定义逻辑。
  • 我们可以实现 beforeTestMethod 或者 beforeTestExecution 方法,在测试方法执行前进行 Stub。

步骤:

  1. 创建一个类,实现 TestExecutionListener 接口。
  2. beforeTestMethodbeforeTestExecution 方法中,从 TestContext 中获取目标 Bean,并使用 Mockito.doNothing().when() 进行 Stub。
  3. 通过 @TestExecutionListeners 注解将自定义 Listener 注册到测试类。

代码示例:

import org.mockito.Mockito;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.TestExecutionListeners;

@SpringBootTest
@TestExecutionListeners(listeners = {EarlyStubbingTestExecutionListener.class}, mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
public abstract class SpringIntegrationBaseTest {

    @SpyBean
    protected ManagementService service;

    @SpyBean
    protected Temp temp;

    static class EarlyStubbingTestExecutionListener implements TestExecutionListener {

        @Override
         public void beforeTestExecution(TestContext testContext) {
            Temp temp = testContext.getApplicationContext().getBean(Temp.class);
             if (temp != null) {
                Mockito.doNothing().when(temp).print();
                 System.out.println("Stubbing print() method of Temp bean in beforeTestExecution");
             }
         }

         // Other callback methods can be left empty or implemented if needed
        @Override public void beforeTestClass(TestContext testContext) {}
        @Override public void prepareTestInstance(TestContext testContext) {}
        @Override public void beforeTestMethod(TestContext testContext) {}
        @Override public void afterTestExecution(TestContext testContext) {}
        @Override public void afterTestMethod(TestContext testContext) {}
        @Override public void afterTestClass(TestContext testContext) {}
    }

}

这段代码中,我们实现了一个EarlyStubbingTestExecutionListener 类。在这个类里,通过 beforeTestExecution 回调方法,从 TestContext 获取了 Temp 的 Bean 实例,然后对print 方法进行了Stubbing。然后使用@TestExecutionListeners 注解,把自定义的 Listener 注册到了 SpringIntegrationBaseTest 。 同样,为了避免和其他TestExecutionListener的冲突,需要使用 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS

安全建议:

  • 确保在 TestExecutionListener 中获取 Bean 的方式是可靠的,例如通过 TestContext.getApplicationContext().getBean()
  • 考虑到 TestExecutionListener 会被多个测试类复用,Stub 操作应避免产生全局副作用,尽量只影响当前测试方法。

方案三:使用 @DirtiesContext 注解

这个方案通过在测试类或测试方法上使用 @DirtiesContext 注解,强制 Spring 重新加载上下文。 这会使得 @BeforeEach 方法在事件监听器触发前执行。

原理:

  • @DirtiesContext 注解指示 Spring 在测试完成后关闭并重新加载应用上下文。
  • @DirtiesContext (classMode=DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) 注解可以使得每个方法执行之前都重新加载 Context 。

步骤:

  1. 在测试基类上添加 `@DirtiesContext(classMode=Dirt