Spring 3.1 M2:使用 @Configuration 类和 Profiles 进行测试

工程 | Sam Brannen | 2011 年 6 月 21 日 | ...

正如 Jürgen Höller 在宣布 Spring 3.1 M2 发布的帖子中提到的那样,Spring TestContext Framework(*) 已经过全面改进,为 @Configuration 类和环境 Profile 提供了第一等的测试支持。

在本文中,我将首先向您介绍一些示例,演示这些新的测试特性。然后,我将介绍 TestContext 框架中使这些新特性成为可能的一些新的扩展点。

      请注意:这是我公司博客 www.swiftmind.com 的交叉发布文章。

背景

在 Spring 2.5 中,我们引入了 Spring TestContext Framework,它提供了注解驱动的集成测试支持,可与 JUnit 或 TestNG 一起使用。本博客中的示例将侧重于基于 JUnit 的测试,但此处使用的所有特性也适用于 TestNG。

TestContext 框架的核心是允许您使用 @ContextConfiguration 注解测试类,以指定用于为测试加载 ApplicationContext 的配置文件。默认情况下,ApplicationContext 使用 GenericXmlContextLoader 加载,后者从 XML Spring 配置文件加载上下文。然后,您可以通过使用 @Autowired@Resource@Inject 注解测试类中的字段来访问 ApplicationContext 中的 bean。

Spring 3.0 通过 @Configuration 类引入了基于 Java 的配置支持,但 TestContext 框架直到现在才提供适当的 ContextLoader 来支持测试中的 @Configuration 类。Spring 3.1 M2 为此引入了一个新的 AnnotationConfigContextLoader,并且 @ContextConfiguration 注解已更新,通过新的 classes 属性支持声明 @Configuration 类。

现在让我们看一些示例。

使用基于 XML 的配置进行集成测试

Spring 参考手册的测试章节提供了许多如何使用 XML 配置文件配置集成测试的示例,但我们在此包含一个示例作为快速介绍。

如果您已经熟悉 Spring TestContext Framework,请随意跳到下一节。


<?xml version="1.0" encoding="UTF-8"?>
<beans ...>

    <!-- this bean will be injected into the OrderServiceTest class -->
    <bean id="orderService" class="com.example.OrderServiceImpl">
        <!-- set properties, etc. -->
    </bean>
    
    <!-- other beans -->

</beans>

package com.example;

@RunWith(SpringJUnit4ClassRunner.class)
// ApplicationContext will be loaded from "classpath:/com/example/OrderServiceTest-context.xml"
@ContextConfiguration
public class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @Test
    public void testOrderService() {
        // test the orderService
    }
}

在前面的示例中,我们配置 JUnit 使用 SpringJUnit4ClassRunner 来运行我们的测试。我们通过使用 JUnit 的 @RunWith 注解来实现这一点。我们还使用 Spring 的 @ContextConfiguration 注解来注解我们的测试类,而没有指定任何属性。在这种情况下,将使用默认的 GenericXmlContextLoader,并且遵循约定优于配置的原则,Spring 将从 classpath:/com/example/OrderServiceTest-context.xml 加载我们的 ApplicationContext。在 testOrderService() 方法中,我们可以直接测试使用 @Autowired 注入到测试实例中的 OrderService。请注意,orderServiceOrderServiceTest-context.xml 中被定义为一个 bean。

使用 @Configuration 类进行集成测试

Spring 3.1 M2 对使用 @Configuration 类进行集成测试的支持类似于上面的基于 XML 的示例。因此,让我们重新调整该示例,使用 @Configuration 类和新的 AnnotationConfigContextLoader


package com.example;

@RunWith(SpringJUnit4ClassRunner.class)
// ApplicationContext will be loaded from the static inner ContextConfiguration class
@ContextConfiguration(loader=AnnotationConfigContextLoader.class)
public class OrderServiceTest {

    @Configuration
    static class ContextConfiguration {

        // this bean will be injected into the OrderServiceTest class
        @Bean
        public OrderService orderService() {
            OrderService orderService = new OrderServiceImpl();
            // set properties, etc.
            return orderService;
        }
    }

    @Autowired
    private OrderService orderService;

    @Test
    public void testOrderService() {
        // test the orderService
    }
}

这个示例与基于 XML 的示例之间有一些显著的区别

  1. 没有 XML 文件。
  2. Bean 定义已使用静态内部类 ContextConfiguration 中的 @Configuration@Bean 从 XML 转换为 Java。
  3. AnnotationConfigContextLoader 已通过 @ContextConfigurationloader 属性指定。

除此之外,测试的配置和实现保持不变。

那么,Spring 是如何知道使用静态内部类 ContextConfiguration 来加载 ApplicationContext 的呢?答案是约定优于配置。默认情况下,如果没有显式声明类,AnnotationConfigContextLoader 将查找测试类中名为 ContextConfiguration 的静态内部类。根据 @Configuration 类的要求,此静态内部类必须是非 final 和非 private 的。

注意:从 Spring 3.1 M2 开始,默认配置类必须精确命名为 ContextConfiguration。然而,从 Spring 3.1 RC1 开始,命名限制已经解除。换句话说,从 RC1 开始,您可以选择任意命名您的默认配置类,但其他要求仍然适用。

在下面的示例中,我们将看到如何声明显式配置类。


package com.example;

@Configuration
public class OrderServiceConfig {

    // this bean will be injected into the OrderServiceTest class
    @Bean
    public OrderService orderService() {
        OrderService orderService = new OrderServiceImpl();
        // set properties, etc.
        return orderService;
    }
}

package com.example;

@RunWith(SpringJUnit4ClassRunner.class)
// ApplicationContext will be loaded from the OrderServiceConfig class
@ContextConfiguration(classes=OrderServiceConfig.class, loader=AnnotationConfigContextLoader.class)
public class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @Test
    public void testOrderService() {
        // test the orderService
    }
}

我们现在已将静态内部类 ContextConfiguration 提取到一个名为 OrderServiceConfig 的顶级类中。要指示 AnnotationConfigContextLoader 使用此配置类而不是依赖默认配置,我们只需通过 @ContextConfiguration 的新 classes 属性声明 OrderServiceConfig.class。与 @ContextConfiguration 用于资源位置的 locations 属性一样,我们可以通过向 classes 属性提供一个 Class[] 数组来声明多个配置类 — 例如:@ContextConfiguration(classes={Config1.class, Config2.class}, ... )

关于使用 @Configuration 类进行集成测试的介绍到此结束。现在让我们看看 Spring 对环境 profile 的测试支持。

使用环境 Profile 进行集成测试

正如 Chris Beams 在 Spring 3.1 M1 的发布公告及其后续博客 Introducing @Profile 中讨论的那样,Spring 3.1 在框架中引入了对环境和 Profile(又称 bean 定义 Profile)概念的一流支持。从 Spring 3.1 M2 开始,还可以配置集成测试,以针对各种测试场景激活特定的 bean 定义 Profile。这可以通过使用新的 @ActiveProfiles 注解来注解测试类,并提供在为测试加载 ApplicationContext 时应激活的 Profile 列表来实现。

注意:@ActiveProfiles 可以与新的 SmartContextLoader SPI 的任何实现一起使用(参见后面的讨论),但 @ActiveProfiles 支持更简单的 ContextLoader SPI 的实现。

让我们来看一些使用 XML 配置和 @Configuration 类的示例。


<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	xmlns:jee="http://www.springframework.org/schema/jee"
	xsi:schemaLocation="...">

	<bean id="transferService" class="com.bank.service.internal.DefaultTransferService">
		<constructor-arg ref="accountRepository"/>
		<constructor-arg ref="feePolicy"/>
	</bean>

	<bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository">
		<constructor-arg ref="dataSource"/>
	</bean>

	<bean id="feePolicy" class="com.bank.service.internal.ZeroFeePolicy"/>

	<beans profile="dev">
		<jdbc:embedded-database id="dataSource">
			<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
			<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
		</jdbc:embedded-database>
	</beans>

	<beans profile="production">
		<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
	</beans>

</beans>

package com.bank.service;

@RunWith(SpringJUnit4ClassRunner.class)
// ApplicationContext will be loaded from "classpath:/app-config.xml"
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
public class TransferServiceTest {

    @Autowired
    private TransferService transferService;

    @Test
    public void testTransferService() {
        // test the transferService
    }
}

运行 TransferServiceTest 时,其 ApplicationContext 将从类路径根目录下的 app-config.xml 配置文件加载。如果您检查 app-config.xml,您会注意到 accountRepository bean 依赖于一个 dataSource bean;然而,dataSource 没有被定义为一个顶级 bean。相反,dataSource 定义了两次:一次在 production Profile 中,一次在 dev Profile 中。

通过使用 @ActiveProfiles("dev") 注解 TransferServiceTest,我们指示 Spring TestContext Framework 加载 ApplicationContext,并将其活动 Profile 设置为 {"dev"}。结果,将创建一个嵌入式数据库,并且 accountRepository bean 将被连接到开发 DataSource。这很可能就是我们在集成测试中想要的!

以下代码清单演示了如何实现相同的配置和集成测试,但使用 @Configuration 类而不是 XML。


@Configuration
@Profile("dev")
public class StandaloneDataConfig {

	@Bean
	public DataSource dataSource() {
		return new EmbeddedDatabaseBuilder()
			.setType(EmbeddedDatabaseType.HSQL)
			.addScript("classpath:com/bank/config/sql/schema.sql")
			.addScript("classpath:com/bank/config/sql/test-data.sql")
			.build();
	}
}

@Configuration
@Profile("production")
public class JndiDataConfig {

	@Bean
	public DataSource dataSource() throws Exception {
		Context ctx = new InitialContext();
		return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
	}
}

@Configuration
public class TransferServiceConfig {

	@Autowired DataSource dataSource;

	@Bean
	public TransferService transferService() {
		return new DefaultTransferService(accountRepository(), feePolicy());
	}

	@Bean
	public AccountRepository accountRepository() {
		return new JdbcAccountRepository(dataSource);
	}

	@Bean
	public FeePolicy feePolicy() {
		return new ZeroFeePolicy();
	}
}

package com.bank.service;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader=AnnotationConfigContextLoader.class,
    classes={TransferServiceConfig.class, StandaloneDataConfig.class, JndiDataConfig.class})
@ActiveProfiles("dev")
public class TransferServiceTest {

    @Autowired
    private TransferService transferService;

    @Test
    public void testTransferService() {
        // test the transferService
    }
}

在此变体中,我们将 XML 配置拆分为三个独立的 @Configuration

  • TransferServiceConfig:使用 @Autowired 通过依赖注入获取 dataSource
  • StandaloneDataConfig:定义一个适用于开发者测试的嵌入式数据库的 dataSource
  • JndiDataConfig:定义一个在生产环境中从 JNDI 获取的 dataSource

与基于 XML 的配置示例一样,我们仍然使用 @ActiveProfiles("dev") 注解 TransferServiceTest,但这次我们通过 @ContextConfiguration 注解指定了 AnnotationConfigContextLoader 和所有三个配置类。测试类本身的主体完全保持不变。

有关如何简化上述 @Configuration 类的详细信息,请参阅Spring 3.1 M1: Introducing @Profile 博客文章。

ApplicationContext 缓存

自 Spring 2.5 以来,Spring TestContext Framework 根据为给定测试合并的所有上下文资源位置生成的 key 来缓存集成测试的 ApplicationContexts。由于 ContextLoader SPI 只支持 locations,这个 key 生成算法足以唯一标识用于加载 ApplicationContext 的配置。然而,随着对配置类和 Profile 的支持增加,旧的算法已不再适用。

因此,Spring 3.1 M2 中更新了上下文缓存 key 生成算法,以包含以下所有内容:

  • locations (来自 @ContextConfiguration)
  • classes (来自 @ContextConfiguration)
  • contextLoader (来自 @ContextConfiguration)
  • activeProfiles (来自 @ActiveProfiles)

作为开发者,这意味着您可以实现一个基础测试类,该类声明一组特定的资源位置或配置类。然后,如果您想针对该基础配置运行测试,但使用不同的活动 Profile,您可以扩展该基础测试类,并使用 @ActiveProfiles 注解每个具体的子类,为每个子类提供一组不同的 Profile 来激活。因此,这些子类中的每一个都将定义一组唯一的配置属性,这将导致加载和缓存不同的 ApplicationContexts

SmartContextLoader 取代 ContextLoader SPI

正如本文前面提到的,Spring 3.1 M2 引入了一个新的 SmartContextLoader SPI,它取代了现有的 ContextLoader SPI。如果您计划开发或已经开发了自己的自定义 ContextLoader,您可能需要仔细研究新的 SmartContextLoader 接口。与旧的 ContextLoader 接口相比,SmartContextLoader 可以处理资源位置和配置类。此外,SmartContextLoader 可以在其加载的上下文中设置活动的 bean 定义 Profile。

ContextLoader 将继续得到支持,并且该 SPI 的任何现有实现都应继续按原样工作;但是,如果您想在自定义加载器中支持配置类或环境 Profile,您将需要实现 SmartContextLoader

DelegatingSmartContextLoader

如果您一直密切关注迄今为止展示的示例,您可能已经注意到,在使用配置类时,我们总是必须为 @ContextConfigurationloader 属性显式声明 AnnotationConfigContextLoader.class。但当我们指定 XML 配置文件(或依赖约定优于配置)时,默认使用 GenericXmlContextLoader

如果 Spring 能判断我们使用的是配置类还是 XML 资源位置,然后自动选择合适的 ContextLoader 来加载我们的应用上下文,那岂不是很好?

是的,我们也这么认为!;)

因此,对于 Spring 3.1 RC1,我们计划引入一个 DelegatingSmartContextLoader,它将委托给一组候选的 SmartContextLoaders(即 GenericXmlContextLoaderAnnotationConfigContextLoader),以确定哪个上下文加载器适用于给定测试类的配置。然后,将使用获胜的候选加载器来实际加载上下文。

这项工作完成后,DelegatingSmartContextLoader 将取代 GenericXmlContextLoader 成为默认加载器。请随时在 JIRA 中关注此开发的进展:SPR-8387

总结

Spring 3.1 为 @Configuration 类和环境 Profile 提供了第一等的测试支持,我们鼓励您尽快试用这些功能。M2 是 3.1 发布列车中的最后一个里程碑。因此,如果您发现任何错误或有任何改进建议,现在就是采取行动的时候了!


(*) 参考手册尚未更新以反映对 @Configuration 类和环境 Profile 的测试支持,但这些功能肯定会在 Spring 3.1 RC1 或 GA 版本发布时得到完善的文档。在此期间,新类和注解的 JavaDoc 可以作为一个很好的起点。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

抢先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 通过一个简单的订阅提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

近期活动

查看 Spring 社区所有近期活动。

查看全部