Spring测试上下文最佳实践

12

我正在尝试用集成测试覆盖一个庞大的Spring Boot应用程序。该应用程序中有许多Spring Bean。加载Spring上下文需要一些时间。

因此,我想知道 -

  • Spring是否聪明到足以在不同类中的多个集成测试之间共享同一个上下文?我的意思是避免为每个测试类初始化沉重的上下文。
  • 当测试1、2、4使用TestContextOne而测试3、5使用TestContextTwo时会发生什么?Spring是否按1、2、4、3、5的顺序启动它们?或者Spring是否在内存中保留两个上下文?

P.S. 换句话说,常见做法是为所有集成测试使用单个“完整的”Spring上下文,而不是为每个测试编写单独的上下文吗?


如果你没有亲自尝试过奇怪的hack操作,理论上它只会加载2个上下文。 - M. Deinum
@M.Deinum 所以通常的做法是为所有集成测试使用一个“完整”的上下文,对吗? - VB_
这要看你想要单元测试、集成测试还是系统测试。你可以很好地编写一个集成测试(针对单个组件),然后仅在进行系统测试时启动该组件(及其依赖项),因为在进行系统测试时,你需要整个系统运行。 - M. Deinum
@M. Deinum是的,每个测试只涉及几个组件。但是我担心设置数百个不同的小测试环境比一个大的环境更昂贵。 - VB_
2个回答

15
Spring框架提供的主要功能之一是上下文缓存机制,用于测试应用程序,以避免您提到的负载开销。Spring文档指出:
一旦TestContext框架为测试加载了ApplicationContext(或WebApplicationContext),该上下文将被缓存并在同一测试套件中声明相同唯一上下文配置的所有后续测试中重复使用。
有了这个断言,您必须了解缓存机制的工作原理,以确定构建测试的最佳策略。问题在于:当Spring缓存上下文时,它使用什么键将此上下文存储在内存中?根据文档,该键基于容器的某些参数。

ApplicationContext 可以通过用于加载它的配置参数的组合来唯一标识。因此,配置参数的唯一组合用于生成一个 key,该键用于缓存上下文。TestContext 框架使用以下配置参数构建上下文缓存键:

locations(来自 @ContextConfiguration)
classes(来自 @ContextConfiguration)
contextInitializerClasses(来自 @ContextConfiguration)
contextCustomizers(来自 ContextCustomizerFactory)
contextLoader(来自 @ContextConfiguration)
parent(来自 @ContextHierarchy)
activeProfiles(来自 @ActiveProfiles)
propertySourceLocations(来自 @TestPropertySource)
propertySourceProperties(来自 @TestPropertySource)
resourceBasePath(来自 @WebAppConfiguration)

根据这些信息,我建议您最好的做法是以一种方式组织您的测试,使它们使用相同的上下文参数集(即相同的缓存键)从缓存机制中受益,并避免加载另一个上下文。Spring文档也给出了一个例子:

如果TestClassA为@ContextConfiguration的locations(或value)属性指定了{"app-config.xml", "test-config.xml"},则TestContext框架将加载相应的ApplicationContext并将其存储在基于这些位置的唯一键下的静态上下文缓存中。因此,如果TestClassB也定义了{"app-config.xml", "test-config.xml"}作为其位置(通过继承明确或隐含地定义),但没有定义@WebAppConfiguration、不同的ContextLoader、不同的活动配置文件、不同的上下文初始化器、不同的测试属性源或不同的父上下文,则两个测试类将共享相同的ApplicationContext。这意味着只有一次(每个测试套件)加载应用程序上下文的设置成本,后续的测试执行速度更快。


3

在集成测试中,您可以使用的另一个技巧是强制将上下文中的所有bean设置为“懒加载”。这在运行单个集成测试时非常有用,因为您不必等待整个应用程序上下文加载和初始化。这可以显着提高单个测试运行所需的时间。

您可能会遇到隐式创建bean的情况(例如:Spring IntegrationFlow)。该流程从未直接注入任何内容,但您的类可能引用了流程创建的bean。在这种情况下,您需要@Autowire您的流程(以确保隐式bean被创建),或者您可以使用BeanPostProcessor进行创意处理。

我创建了以下后处理器,您只需将其添加到测试spring上下文中即可。

public class LazyInitBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    private Class<?>[] exclusionList;

    public LazyInitBeanFactoryPostProcessor() {
    }

    public LazyInitBeanFactoryPostProcessor(Class<?>[] exclusionList) {
        this.exclusionList = exclusionList;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

        //Iterate over all bean, mark them as lazy if they are not in the exclusion list.
        for (String beanName : beanFactory.getBeanDefinitionNames()) {
            if (isLazy(beanName, beanFactory)) {
                BeanDefinition definition = beanFactory.getBeanDefinition(beanName);
                definition.setLazyInit(true);
            }
        }
    }

    private boolean isLazy(String beanName, ConfigurableListableBeanFactory beanFactory) {
        if (exclusionList == null || exclusionList.length == 0) {
            return true;
        }
        for (Class<?> clazz : exclusionList) {
            if (beanFactory.isTypeMatch(beanName,clazz)) {
                return false;
            } 
        } 
        return true;        
    }
}

使用方法:

@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class MyTest {
.
.
.
@TestConfiguration
protected static class TestConfiguration {

    @Bean
    public BeanFactoryPostProcessor lazyBeanPostProcessor() {
        return new LazyInitBeanFactoryPostProcessor();
    }
}

或者您可以通过“排除”方式来扩展它(在这个例子中,任何可分配给Spring Integration流的bean将不会被标记为延迟加载:

@TestConfiguration
protected static class TestConfiguration {
    @Bean
    public BeanFactoryPostProcessor lazyBeanPostProcessor() {
        return new ExtendedTestLazyBeanFactoryPostProcessor();
    }


    static private class ExtendedTestLazyBeanFactoryPostProcessor extends LazyInitBeanFactoryPostProcessor {    
        public ServiceTestLazyBeanFactoryPostProcessor() {
            super(new Class<?>[] {IntegrationFlow.class});
        }   
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接