抢先一步
VMware 提供培训和认证,助您加速进步。
了解更多正如 Jürgen Höller 在宣布 Spring 3.1 M2 发布的帖子中提到的那样,Spring TestContext Framework(*) 已经过全面改进,为 @Configuration 类和环境 Profile 提供了第一等的测试支持。
在本文中,我将首先向您介绍一些示例,演示这些新的测试特性。然后,我将介绍 TestContext 框架中使这些新特性成为可能的一些新的扩展点。
请注意:这是我公司博客 www.swiftmind.com 的交叉发布文章。
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 类。
现在让我们看一些示例。
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。请注意,orderService 在 OrderServiceTest-context.xml 中被定义为一个 bean。
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 的示例之间有一些显著的区别
ContextConfiguration 中的 @Configuration 和 @Bean 从 XML 转换为 Java。AnnotationConfigContextLoader 已通过 @ContextConfiguration 的 loader 属性指定。除此之外,测试的配置和实现保持不变。
那么,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 的测试支持。
正如 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 通过依赖注入获取 dataSourceStandaloneDataConfig:定义一个适用于开发者测试的嵌入式数据库的 dataSourceJndiDataConfig:定义一个在生产环境中从 JNDI 获取的 dataSource与基于 XML 的配置示例一样,我们仍然使用 @ActiveProfiles("dev") 注解 TransferServiceTest,但这次我们通过 @ContextConfiguration 注解指定了 AnnotationConfigContextLoader 和所有三个配置类。测试类本身的主体完全保持不变。
有关如何简化上述 @Configuration 类的详细信息,请参阅Spring 3.1 M1: Introducing @Profile 博客文章。
自 Spring 2.5 以来,Spring TestContext Framework 根据为给定测试合并的所有上下文资源位置生成的 key 来缓存集成测试的 ApplicationContexts。由于 ContextLoader SPI 只支持 locations,这个 key 生成算法足以唯一标识用于加载 ApplicationContext 的配置。然而,随着对配置类和 Profile 的支持增加,旧的算法已不再适用。
因此,Spring 3.1 M2 中更新了上下文缓存 key 生成算法,以包含以下所有内容:
@ContextConfiguration)@ContextConfiguration)@ContextConfiguration)@ActiveProfiles)作为开发者,这意味着您可以实现一个基础测试类,该类声明一组特定的资源位置或配置类。然后,如果您想针对该基础配置运行测试,但使用不同的活动 Profile,您可以扩展该基础测试类,并使用 @ActiveProfiles 注解每个具体的子类,为每个子类提供一组不同的 Profile 来激活。因此,这些子类中的每一个都将定义一组唯一的配置属性,这将导致加载和缓存不同的 ApplicationContexts。
正如本文前面提到的,Spring 3.1 M2 引入了一个新的 SmartContextLoader SPI,它取代了现有的 ContextLoader SPI。如果您计划开发或已经开发了自己的自定义 ContextLoader,您可能需要仔细研究新的 SmartContextLoader 接口。与旧的 ContextLoader 接口相比,SmartContextLoader 可以处理资源位置和配置类。此外,SmartContextLoader 可以在其加载的上下文中设置活动的 bean 定义 Profile。
ContextLoader 将继续得到支持,并且该 SPI 的任何现有实现都应继续按原样工作;但是,如果您想在自定义加载器中支持配置类或环境 Profile,您将需要实现 SmartContextLoader。
如果您一直密切关注迄今为止展示的示例,您可能已经注意到,在使用配置类时,我们总是必须为 @ContextConfiguration 的 loader 属性显式声明 AnnotationConfigContextLoader.class。但当我们指定 XML 配置文件(或依赖约定优于配置)时,默认使用 GenericXmlContextLoader。
如果 Spring 能判断我们使用的是配置类还是 XML 资源位置,然后自动选择合适的 ContextLoader 来加载我们的应用上下文,那岂不是很好?
是的,我们也这么认为!;)
因此,对于 Spring 3.1 RC1,我们计划引入一个 DelegatingSmartContextLoader,它将委托给一组候选的 SmartContextLoaders(即 GenericXmlContextLoader 和 AnnotationConfigContextLoader),以确定哪个上下文加载器适用于给定测试类的配置。然后,将使用获胜的候选加载器来实际加载上下文。
这项工作完成后,DelegatingSmartContextLoader 将取代 GenericXmlContextLoader 成为默认加载器。请随时在 JIRA 中关注此开发的进展:SPR-8387。
Spring 3.1 为 @Configuration 类和环境 Profile 提供了第一等的测试支持,我们鼓励您尽快试用这些功能。M2 是 3.1 发布列车中的最后一个里程碑。因此,如果您发现任何错误或有任何改进建议,现在就是采取行动的时候了!
(*) 参考手册尚未更新以反映对 @Configuration 类和环境 Profile 的测试支持,但这些功能肯定会在 Spring 3.1 RC1 或 GA 版本发布时得到完善的文档。在此期间,新类和注解的 JavaDoc 可以作为一个很好的起点。