每个 TestContext 为它所负责的测试实例提供上下文管理和缓存支持。测试实例不会自动收到对配置的 ApplicationContext 的访问。然而,如果一个测试类实现了 ApplicationContextAware 接口,对 ApplicationContext 的引用将提供给测试实例。请注意,AbstractJUnit4SpringContextTests 和 AbstractTestNGSpringContextTests 实现了 ApplicationContextAware,因此,自动提供对 ApplicationContext 的访问。
@Autowired ApplicationContext
作为实现 ApplicationContextAware 接口的替代方案,你可以通过字段或 setter 方法上的 @Autowired 注解为你的测试类注入应用上下文,如下例所示。
@SpringJUnitConfig
class MyTest {
@Autowired
ApplicationContext applicationContext;
// class body...
}
同样,如果你的测试被配置为加载一个 WebApplicationContext,你可以将 Web 应用上下文注入你的测试,如下所示:
@SpringJUnitWebConfig
class MyWebAppTest {
@Autowired
WebApplicationContext wac;
// class body...
}
通过使用 @Autowired 的依赖注入是由 DependencyInjectionTestExecutionListener 提供的,它是默认配置的(见测试夹具的依赖注入)。
使用 TestContext 框架的测试类不需要扩展任何特定的类或实现一个特定的接口来配置他们的应用上下文。相反,配置是通过在类级别声明@ContextConfiguration
注解来实现的。如果你的测试类没有明确声明应用上下文资源位置或组件类,那么配置的 ContextLoader 会决定如何从默认位置或默认配置类中加载上下文。除了上下文资源位置和组件类,应用上下文还可以通过应用上下文初始化器进行配置。
下面几节将解释如何使用 Spring 的 @ContextConfiguration
注解,通过使用 XML 配置文件、Groovy 脚本、组件类(通常是@Configuration
类)或上下文初始化器来配置测试 ApplicationContext。另外,你也可以为高级用例实现和配置自己的自定义 SmartContextLoader。
- 用 XML 资源进行上下文配置
- 用 Groovy 脚本进行上下文配置
- 使用组件类的上下文配置
- 混合使用 XML、Groovy 脚本和组件类
- 使用上下文初始化器的上下文配置
- 上下文配置的继承性
- 使用环境配置文件的上下文配置
- 使用测试属性源的上下文配置
- 使用动态属性源的上下文配置
- 加载一个 WebApplicationContext
- 上下文缓存
- 上下文层次结构
用 XML 资源进行上下文配置
要通过使用 XML 配置文件为你的测试加载 ApplicationContext,用 @ContextConfiguration
注解你的测试类,并用一个包含 XML 配置元数据资源位置的数组来配置 locations 属性。一个普通的或相对的路径(例如,context.xml)被视为一个 classpath 资源,它是相对于测试类所定义的包的。一个以斜线开头的路径被视为一个绝对的 classpath 位置(例如,/org/example/config.xml
)。代表资源 URL 的路径(即以 classpath:
、file:
、http:
等为前缀的路径)被原样使用。
@ExtendWith(SpringExtension.class)
// ApplicationContext 将从 /app-config.xml 和
// classpath 根目录中的 /test-config.xml 加载 ApplicationContext
@ContextConfiguration(locations={"/app-config.xml", "/test-config.xml"})
class MyTest {
// class body...
}
@ContextConfiguration 通过标准的 Java 值属性支持 locations 属性的别名。因此,如果你不需要在 @ContextConfiguration 中声明额外的属性,你可以省略对 locations 属性名称的声明,并通过使用下面例子中演示的速记格式来声明资源位置:
@ExtendWith(SpringExtension.class)
// 在不使用 location 属性的情况下指定 XML 文件。
@ContextConfiguration({"/app-config.xml", "/test-config.xml"})
class MyTest {
// class body...
}
如果你从 @ContextConfiguration 注解中省略了 locations 和 value 属性,TestContext 框架会尝试检测一个默认的 XML 资源位置。具体来说,GenericXmlContextLoader 和 GenericXmlWebContextLoader 会根据测试类的名称检测一个默认位置。如果你的类被命名为com.example.MyTest
,GenericXmlContextLoader 会从 classpath:com/example/MyTest-context.xml
加载你的应用程序上下文。下面的例子显示了如何做到这一点:
@ExtendWith(SpringExtension.class)
// 从默认位置加载配置。
// ApplicationContext 将从
// "classpath:com/example/MyTest-context.xml" 加载
@ContextConfiguration
class MyTest {
// class body...
}
用 Groovy 脚本进行上下文配置
要通过使用 Groovy Bean Definition DSL 的 Groovy 脚本为你的测试加载 ApplicationContext,你可以用 @ContextConfiguration 注解你的测试类,并用一个包含 Groovy 脚本资源位置的数组配置 locations 或 value 属性。Groovy 脚本的资源查找语义与 XML 配置文件的描述相同。
:::tips 启用 Groovy 脚本支持
如果 Groovy 在 classpath 上,就会自动启用对使用 Groovy 脚本在 Spring TestContext 框架中加载 ApplicationContext 的支持。 :::
@ExtendWith(SpringExtension.class)
// 从指定位置加载
// ApplicationContext will be loaded from "/AppConfig.groovy" and
// "/TestConfig.groovy" in the root of the classpath
@ContextConfiguration({"/AppConfig.groovy", "/TestConfig.Groovy"})
class MyTest {
// class body...
}
@ExtendWith(SpringExtension.class)
// 从默认位置加载
// ApplicationContext will be loaded from
// "classpath:com/example/MyTestContext.groovy"
@ContextConfiguration
class MyTest {
// class body...
}
同时声明 XML 配置和 Groovy 脚本
你可以通过使用 @ContextConfiguration 的 locations 或 value 属性同时声明 XML 配置文件和 Groovy 脚本。如果配置的资源位置的路径以 .xml
结尾,它将通过使用 XmlBeanDefinitionReader 加载。否则,它将通过使用 GroovyBeanDefinitionReader 来加载:
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from
// "/app-config.xml" and "/TestConfig.groovy"
@ContextConfiguration({ "/app-config.xml", "/TestConfig.groovy" })
class MyTest {
// class body...
}
下面的列表显示了如何在集成测试中结合两者。
使用组件类的上下文配置
要通过使用组件类为你的测试加载 ApplicationContext(见基于 Java 的容器配置),你可以用 @ContextConfiguration 注解你的测试类,并用一个包含对组件类引用的数组配置 classes 属性。下面的例子显示了如何做到这一点:
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from AppConfig and TestConfig
// 指定组件类
@ContextConfiguration(classes = {AppConfig.class, TestConfig.class})
class MyTest {
// class body...
}
:::tips 组件类
术语 「组件类(component class)」可以指的是以下任何一种:
- 一个用 @Configuration 注解的类。
- 一个组件(即一个用 @Component、@Service、@Repository 或其他定型注解的类)。
- 一个符合 JSR-330 的类,它被 javax.inject 注解所注解。
- 任何包含 @Bean 方法的类。
- 任何其他打算注册为 Spring 组件的类(即 ApplicationContext 中的 Spring Bean),有可能利用单个构造函数的自动连接而不使用Spring 注解。
有关组件类的配置和语义的进一步信息,请参见 @Configuration 和 @Bean 的 javadoc,要特别注意 @Bean Lite 模式的讨论。 :::
如果你省略了 @ContextConfiguration 注解中的 classes 属性,TestContext 框架会尝试检测默认配置类的存在。具体来说,AnnotationConfigContextLoader 和 AnnotationConfigWebContextLoader 会检测测试类的所有静态嵌套类,这些类符合配置类实现的要求,如 @Configuration javadoc 中规定的。注意,配置类的名字是任意的。此外,如果需要,一个测试类可以包含一个以上的静态嵌套配置类。在下面的例子中,OrderServiceTest 类声明了一个名为 Config 的静态嵌套配置类,它被自动用于加载测试类的 ApplicationContext。
@SpringJUnitConfig
// ApplicationContext 将被加载自静态嵌套的配置类
// 从嵌套的 Config 类中加载配置信息。
class OrderServiceTest {
@Configuration
static class Config {
// 这个 Bean 将被注入到 OrderServiceTest 类中。
@Bean
OrderService orderService() {
OrderService orderService = new OrderServiceImpl();
// set properties, etc.
return orderService;
}
}
@Autowired
OrderService orderService;
@Test
void testOrderService() {
// test the orderService
}
}
混合使用 XML、Groovy 脚本和组件类
有时可能需要混合 XML 配置文件、Groovy 脚本和组件类(通常是 @Configuration 类)来为你的测试配置一个 ApplicationContext。例如,如果你在生产中使用 XML 配置,你可能决定要使用 @Configuration 类来为你的测试配置特定的 Spring 管理的组件,反之亦然。
此外,一些第三方框架(如 Spring Boot)为同时从不同类型的资源(例如,XML 配置文件、Groovy 脚本和 @Configuration类)加载ApplicationContext 提供了一流的支持。从历史上看,Spring 框架并不支持标准部署。因此,Spring 框架在 spring-test 模块中提供的大多数SmartContextLoader 实现,对每个测试上下文只支持一种资源类型。然而,这并不意味着你不能同时使用两种资源。一般规则的一个例外是,GenericGroovyXmlContextLoader 和 GenericGroovyXmlWebContextLoader 同时支持 XML 配置文件和 Groovy 脚本。此外,第三方框架可以选择通过 @ContextConfiguration 来支持位置和类的声明,而且,在 TestContext 框架的标准测试支持下,你有以下选择。
如果你想使用资源位置(例如,XML 或 Groovy)和 @Configuration 类来配置你的测试,你必须选择一个作为入口点,而且这个入口点必须包括或导入另一个。例如,在 XML 或 Groovy 脚本中,你可以通过使用组件扫描或将它们定义为正常的 Spring Bean 来包含 @Configuration 类,而在 @Configuration 类中,你可以使用 @ImportResource 来导入 XML 配置文件或 Groovy 脚本。请注意,这种行为在语义上等同于你在生产中配置你的应用程序的方式。在生产配置中,你定义了一组 XML 或 Groovy 资源位置或一组 @Configuration 类,你的生产 ApplicationContext 就是从这里加载的,但你仍然可以自由地包含或导入其他类型的配置。
使用上下文初始化器的上下文配置
要通过使用上下文初始化器为你的测试配置 ApplicationContext,用 @ContextConfiguration 注解你的测试类,并用一个数组配置初始化器属性,其中包含对实现 ApplicationContextInitializer 的类的引用。然后,声明的上下文初始化器被用来初始化为你的测试加载的ConfigurableApplicationContext。注意,每个声明的初始化器所支持的具体的 ConfigurableApplicationContext 类型必须与使用中的 SmartContextLoader 所创建的 ApplicationContext 类型兼容(通常是 GenericApplicationContext)。此外,初始化器被调用的顺序取决于它们是否实现了 Spring 的有序接口或被 Spring 的 @Order 注解或标准的 @Priority 注解所注释。下面的例子展示了如何使用初始化器。
@ExtendWith(SpringExtension.class)
// // ApplicationContext将从TestConfig加载。
@ContextConfiguration(
classes = TestConfig.class,
// 通过使用一个配置类和一个初始化器来指定配置。
initializers = TestAppCtxInitializer.class)
class MyTest {
// class body...
}
你也可以完全省略 @ContextConfiguration 中对 XML 配置文件、Groovy 脚本或组件类的声明,而只声明 ApplicationContextInitializer 类,然后由这些类负责在上下文中注册 Bean ,例如,通过编程方式从 XML 文件或配置类加载 Bean 定义。下面的例子展示了如何做到这一点。
@ExtendWith(SpringExtension.class)
// ApplicationContext 将被 EntireAppInitializer 初始化。它可能会在上下文中注册 bean 类。
@ContextConfiguration(initializers = EntireAppInitializer.class)
class MyTest {
// class body...
}
上下文配置的继承性
@ContextConfiguration 支持布尔值 inheritLocations 和 inheritInitializers 属性,表示是否应该继承超类所声明的资源位置或组件类和上下文初始化器。这两个标志的默认值是 true。这意味着测试类会继承任何超类所声明的资源位置或组件类,以及上下文初始化器。具体来说,一个测试类的资源位置或组件类被附加到超类所声明的资源位置或注解类的列表中。同样地,一个给定的测试类的初始化器被添加到测试超类定义的初始化器集合中。因此,子类可以选择扩展资源位置、组件类或上下文初始化器。
如果 @ContextConfiguration 中的 inheritLocations 或 inheritInitializers 属性被设置为 false,那么测试类的资源位置或组件类和上下文初始化器就会分别产生阴影,并有效地取代由超类定义的配置。
:::info 从 Spring Framework 5.3 开始,测试配置也可以从嵌套的类中继承下来。详见 @Nested test class configuration。 :::
在下一个使用 XML 资源位置的例子中,ExtendedTest 的 ApplicationContext 依次从 base-config.xml 和 extended-config.xml 中加载。因此,在 extended-config.xml 中定义的 Bean 可以覆盖(也就是替换)base-config.xml 中定义的 Bean。下面的例子显示了一个类如何扩展另一个类并同时使用它自己的配置文件和超类的配置文件。
@ExtendWith(SpringExtension.class)
@ContextConfiguration("/base-config.xml")
class BaseTest {
// class body...
}
//ApplicationContext 将从 classpath 根部的 /base-config.xml 和 /extended-config.xml 加载。
@ContextConfiguration("/extended-config.xml")
class ExtendedTest extends BaseTest {
// class body...
}
同样,在下一个使用组件类的例子中,ExtendedTest 的 ApplicationContext 依次从 BaseConfig 和 ExtendedConfig 类加载。因此,在 ExtendedConfig 中定义的 Bean 可以覆盖(也就是替换)BaseConfig 中定义的 Bean。下面的例子显示了一个类如何扩展另一个类并同时使用它自己的配置类和超类的配置类:
// ApplicationContext will be loaded from BaseConfig
@SpringJUnitConfig(BaseConfig.class)
class BaseTest {
// class body...
}
// ApplicationContext will be loaded from BaseConfig and ExtendedConfig
@SpringJUnitConfig(ExtendedConfig.class)
class ExtendedTest extends BaseTest {
// class body...
}
在下一个使用上下文初始化器的例子中,ExtendedTest 的 ApplicationContext 是通过使用 BaseInitializer 和 ExtendedInitializer 进行初始化的。但是请注意,初始化器被调用的顺序取决于它们是否实现了 Spring 的有序接口,或被 Spring 的 @Order 注解或标准的 @Priority 注解所注释。下面的例子显示了一个类如何扩展另一个类并同时使用自己的初始化器和超类的初始化器:
// ApplicationContext will be initialized by BaseInitializer
@SpringJUnitConfig(initializers = BaseInitializer.class)
class BaseTest {
// class body...
}
// ApplicationContext will be initialized by BaseInitializer
// and ExtendedInitializer
@SpringJUnitConfig(initializers = ExtendedInitializer.class)
class ExtendedTest extends BaseTest {
// class body...
}
使用环境配置文件的上下文配置
Spring 框架对环境和配置文件(AKA(又叫) “bean definition profiles”)的概念有一流的支持,集成测试可以被配置为激活各种测试场景的特定bean definition profiles。这可以通过用 @ActiveProfiles 注解来实现,并提供一个在加载测试的 ApplicationContext 时应该被激活的配置文件列表。
:::info 你可以在 SmartContextLoader SPI 的任何实现中使用 @ActiveProfiles,但 @ActiveProfiles 不支持旧的 ContextLoader SPI 的实现。 :::
考虑两个带有 XML 配置和 @Configuration 类的例子:
<!-- app-config.xml -->
<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 profile="default">
<jdbc:embedded-database id="dataSource">
<jdbc:script
location="classpath:com/bank/config/sql/schema.sql"/>
</jdbc:embedded-database>
</beans>
</beans>
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from "classpath:/app-config.xml"
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
class TransferServiceTest {
@Autowired
TransferService transferService;
@Test
void testTransferService() {
// test the transferService
}
}
当 TransferServiceTest 运行时,它的 ApplicationContext 是从 classpath 根目录中的 app-config.xml 配置文件中加载的。如果你检查 app-config.xml,你可以看到 accountRepository bean 对 dataSource bean 有依赖性。然而,dataSource 并没有被定义为顶级 Bean。相反, dataSource 被定义了三次:在生产配置文件、开发配置文件和默认配置文件中。
通过用 @ActiveProfiles("dev")
注解 TransferServiceTest,我们指示 Spring TestContext 框架加载 ApplicationContext,并将活动配置文件设置为 {"dev"}
。结果是,一个嵌入式数据库被创建并填充了测试数据,而且 accountRepository Bean 被连接到了开发数据源的引用。这可能就是我们在集成测试中想要的。
有时,将 bean 分配到一个 default 的配置文件是很有用的。只有在没有特别激活其他配置文件的情况下,默认配置文件中的 Bean 才会被包含。你可以用它来定义 后备(fallback) Bean,以便在应用程序的默认状态下使用。例如,你可以明确地为开发和生产配置文件提供一个数据源,但当这两个配置文件都没有激活时,你可以定义一个内存数据源作为默认。
下面的代码列表演示了如何用 @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(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
@Configuration
@Profile("default")
public class DefaultDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.build();
}
}
@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();
}
}
@SpringJUnitConfig({
TransferServiceConfig.class,
StandaloneDataConfig.class,
JndiDataConfig.class,
DefaultDataConfig.class})
@ActiveProfiles("dev")
class TransferServiceTest {
@Autowired
TransferService transferService;
@Test
void testTransferService() {
// test the transferService
}
}
在这个变体中,我们把 XML 配置分成四个独立的 @Configuration 类。
- TransferServiceConfig:通过使用 @Autowired,通过依赖性注入获得一个数据源。
- StandaloneDataConfig:为适合开发者测试的嵌入式数据库定义一个数据源。
- JndiDataConfig:定义了一个在生产环境中从 JNDI 检索的数据源。
- DefaultDataConfig:为默认的嵌入式数据库定义一个数据源,在没有配置文件的情况下。
和基于 XML 的配置例子一样,我们仍然用 @ActiveProfiles("dev")
来注解 TransferServiceTest,但这次我们通过使用@ContextConfiguration
注解来指定所有四个配置类。测试类的主体本身保持完全不变。
通常的情况是,在一个给定的项目中,一组配置文件被用于多个测试类。因此,为了避免重复声明 @ActiveProfiles
注解,你可以在基类上声明一次 @ActiveProfiles
,子类自动从基类继承 @ActiveProfiles
配置。在下面的例子中,@ActiveProfiles
的声明(以及其他注解)已经被移到了抽象的超类 AbstractIntegrationTest。
:::info 从 Spring Framework 5.3 开始,测试配置也可以从封闭类继承。有关详细信息,请参见 @Nested test class 配置。 :::
@SpringJUnitConfig({
TransferServiceConfig.class,
StandaloneDataConfig.class,
JndiDataConfig.class,
DefaultDataConfig.class})
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
// "dev" profile inherited from superclass
class TransferServiceTest extends AbstractIntegrationTest {
@Autowired
TransferService transferService;
@Test
void testTransferService() {
// test the transferService
}
}
@ActiveProfiles 还支持一个 inheritProfiles 属性,可以用来禁用活动配置文件的继承,如下例所示。
// "dev" 被 "production" 覆盖
// 意思就是说:dev 这个环境不会被激活,被禁用掉了
@ActiveProfiles(profiles = "production", inheritProfiles = false)
class ProductionTransferServiceTest extends AbstractIntegrationTest {
// test body
}
此外,有时需要以编程方式而不是声明方式解决测试的活动配置文件,例如,基于:
- 当前的操作系统。
- 测试是否正在持续集成构建服务器上运行。
- 某些环境变量的存在。
- 自定义类级注解的存在。
- 其他关注点。
为了以编程方式解析活动 bean 义配置文件,你可以实现一个自定义的 ActiveProfilesResolver 并通过使用 @ActiveProfiles 的 resolver 属性来注册它。关于更多信息,请看相应的 javadoc。下面的例子演示了如何实现和注册一个自定义 OperatingSystemActiveProfilesResolver:
// 禁用继承
// 然后以编程的方式返回活动的环境名称
@ActiveProfiles(
resolver = OperatingSystemActiveProfilesResolver.class,
inheritProfiles = false)
class TransferServiceTest extends AbstractIntegrationTest {
// test body
}
public class OperatingSystemActiveProfilesResolver implements ActiveProfilesResolver {
@Override
public String[] resolve(Class<?> testClass) {
String profile = ...;
// 根据操作系统确定配置文件的值
return new String[] {profile};
}
}
使用测试属性源的上下文配置
Spring 框架对具有属性源层次结构的环境概念有一流的支持,你可以用测试特定的属性源配置集成测试。与 @Configuration 类上使用的@PropertySource 注解不同,你可以在测试类上声明 @TestPropertySource 注解,以声明测试属性文件或内联属性的资源位置。这些测试属性源被添加到环境中的 PropertySources 集合中,用于加载注解的集成测试的 ApplicationContext。
:::info 你可以在 SmartContextLoader SPI 的任何实现中使用 @TestPropertySource,但 @TestPropertySource 不支持旧的 ContextLoader SPI 的实现。
SmartContextLoader 的实现通过 MergedContextConfiguration 中的 getPropertySourceLocations() 和 getPropertySourceProperties() 方法获得对合并的测试属性源值的访问。 :::
声明测试属性源
你可以通过使用 @TestPropertySource 的 locations 或 value 属性来配置测试属性文件。
传统的和基于 XML 的属性文件格式都被支持,例如,classpath:/com/example/test.properties
或 file://path/to/file.xml
。
每个路径都被解释为一个 Spring Resource
。一个普通的路径(例如,test.properties
)被视为一个 classpath 资源,它是相对于测试类所定义的包而言的。以斜线开头的路径被视为绝对的 classpath 资源(例如:/org/example/test.xml
)。引用 URL 的路径(例如,以classpath:
、file:
或 http:
为前缀的路径)通过使用指定的资源协议加载。资源位置通配符(如 */.properties
)是不允许的:每个位置必须精确地评估为一个 .properties
或 .xml
资源。
下面的例子使用了一个测试属性文件:
@ContextConfiguration
// 指定一个具有绝对路径的属性文件。
@TestPropertySource("/test.properties")
class MyIntegrationTests {
// class body...
}
你可以通过使用 @TestPropertySource 的属性,以键值对的形式配置内联属性,如下例所示。所有的键值对被添加到包围的环境中,作为一个具有最高优先级的单一测试 PropertySource。
支持的键值对的语法与为 Java 属性文件中的条目定义的语法相同:
key=value
key:value
key value
以下示例设置了两个内联属性:
@ContextConfiguration
// 通过使用键值语法的两种变化来设置两个属性。
@TestPropertySource(properties = {"timezone = GMT", "port: 4242"})
class MyIntegrationTests {
// class body...
}
:::info 从 Spring Framework 5.2 开始,@TestPropertySource 可以作为可重复注解使用。这意味着你可以在一个测试类上多次声明@TestPropertySource,后面的 @TestPropertySource 注解的位置和属性会覆盖之前的 @TestPropertySource 注解的内容。
此外,你可以在一个测试类上声明多个组成注解,每个注解都用 @TestPropertySource 进行元注解,所有这些 @TestPropertySource 声明都将有助于你的测试属性源。
直接存在的 @TestPropertySource 注解总是优先于元存在的 @TestPropertySource 注解。换句话说,来自直接存在的 @TestPropertySource 注解的位置和属性将覆盖来自作为元注解的 @TestPropertySource 注解的位置和属性。 :::
默认属性文件检测
如果 @TestPropertySource 被声明为一个空注解(也就是说,没有明确的位置或属性值),就会尝试检测相对于声明该注解的类的默认属性文件。例如,如果被注解的测试类是 com.example.MyTest
,相应的默认属性文件是 classpath:com/example/MyTest.properties
。如果不能检测到默认,就会抛出一个 IllegalStateException。
优先级
测试属性比在操作系统环境中定义的属性、Java 系统属性或应用程序通过使用 @PropertySource 声明性地或以编程方式添加的属性源 有更高的优先权。因此,测试属性可以被用来选择性地覆盖从系统和应用程序属性源加载的属性。此外,内联的属性比从资源位置加载的属性有更高的优先权。然而,请注意,通过 @DynamicPropertySource
注册的属性比通过 @TestPropertySource
加载的属性有更高的优先权。
在下一个例子中,时区和端口属性以及 /test.properties
中定义的任何属性都优先于系统和应用程序属性源中定义的同名属性。此外,如果 /test.properties
文件为时区和端口属性定义了条目,这些条目会被通过使用属性声明的内联属性覆盖。下面的例子显示了如何在文件和内联中指定属性:
@ContextConfiguration
// locations 会覆盖系统或则应用程序中的同名属性
// 而 properties 这个内连属性又会覆盖 locations 的属性
@TestPropertySource(
locations = "/test.properties",
properties = {"timezone = GMT", "port: 4242"}
)
class MyIntegrationTests {
// class body...
}
继承和重写测试属性源
@TestPropertySource 支持布尔值 inheritLocations 和 inheritProperties 属性,表示是否应该继承由超类声明的属性文件和内联属性的资源位置。这两个标志的默认值是 true。这意味着测试类会继承任何超类所声明的位置和内联属性。具体来说,测试类的位置和内联属性被附加到超类所声明的位置和内联属性上。因此,子类可以选择扩展位置和内联属性。注意,后面出现的属性会影射(也就是覆盖)前面出现的同名属性。此外,前面提到的优先规则也适用于继承的测试属性源。
如果 @TestPropertySource
中的 inheritLocations 或 inheritProperties 属性被设置为 false,那么测试类的 location 或 inlined 属性将分别对超类定义的配置产生阴影并有效地进行替换(也就是不继承超类中的配置定义)。
:::info 从 Spring Framework 5.3 开始,测试配置也可以从嵌套的类中继承下来。详见 @Nested test class configuration。 :::
在下一个例子中,BaseTest 的 ApplicationContext 是通过只使用 base.properties 文件作为测试属性源来加载的。与此相反,ExtendedTest 的 ApplicationContext 是通过使用 base.properties 和 extended.properties 文件作为测试属性源位置来加载的。下面的例子显示了如何通过使用属性文件在子类和它的超类中定义属性。
@TestPropertySource("base.properties")
@ContextConfiguration
class BaseTest {
// ...
}
@TestPropertySource("extended.properties")
@ContextConfiguration
class ExtendedTest extends BaseTest {
// ...
}
在下一个例子中,BaseTest 的 ApplicationContext 只通过使用内联的 key1 属性被加载。与此相反,ExtendedTest 的 ApplicationContext 是通过使用内联的 key1 和 key2 属性加载的。下面的例子显示了如何通过使用内联属性在子类和它的超类中定义属性。
@TestPropertySource(properties = "key1 = value1")
@ContextConfiguration
class BaseTest {
// ...
}
@TestPropertySource(properties = "key2 = value2")
@ContextConfiguration
class ExtendedTest extends BaseTest {
// ...
}
使用动态属性源的上下文配置
从 Spring Framework 5.2.5 开始,TestContext 框架通过 @DynamicPropertySource
注解提供了对动态属性的支持。这个注解可用于集成测试,它需要将具有动态值的属性添加到集成测试加载的 ApplicationContext 的环境中的 PropertySources 集合。
:::info @DynamicPropertySource 注解及其支持的基础结构最初是为了让基于 Testcontainers 的测试的属性能够轻松地暴露给 Spring 集成测试。然而,该功能也可用于任何形式的外部资源,其生命周期在测试的 ApplicationContext 之外被维护。 :::
与应用于类级别的 @TestPropertySource 注解不同,@DynamicPropertySource 必须应用于一个静态方法,该方法接受一个DynamicPropertyRegistry 参数,用于向环境添加名-值对。值是动态的,并通过一个 Supplier 提供,该 Supplier 只有在属性被解析时才会被调用。通常情况下,方法引用被用来提供值,正如在下面的例子中所看到的,它使用 Testcontainers 项目来管理 Spring ApplicationContext 之外的 Redis 容器。管理的 Redis 容器的 IP 地址和端口通过 redis.host 和 redis.port 属性提供给测试的 ApplicationContext 中的组件。这些属性可以通过 Spring 的环境抽象来访问,也可以直接注入 Spring 管理的组件中—例如,分别通过 @Value("${redis.host}")
和@Value("${redis.port}")
。
:::tips 如果你在基类中使用了 @DynamicPropertySource,并且发现子类中的测试因为动态属性在子类之间改变而失败,你可能需要用@DirtiesContext 来注解你的基类,以确保每个子类得到它自己的具有正确动态属性的 ApplicationContext。 :::
@SpringJUnitConfig(/* ... */)
@Testcontainers
class ExampleIntegrationTests {
@Container
static RedisContainer redis = new RedisContainer();
@DynamicPropertySource
static void redisProperties(DynamicPropertyRegistry registry) {
registry.add("redis.host", redis::getContainerIpAddress);
registry.add("redis.port", redis::getMappedPort);
}
// tests ...
}
优先级
动态属性比那些从 @TestPropertySource、操作系统的环境、Java 系统属性或应用程序通过使用 @PropertySource 声明性地或以编程方式添加的属性源加载的属性具有更高的优先权。因此,动态属性可以用来选择性地覆盖通过 @TestPropertySource、系统属性源和应用程序属性源加载的属性。
加载一个 WebApplicationContext
为了指示 TestContext 框架加载 WebApplicationContext 而不是标准的 ApplicationContext,你可以用 @WebAppConfiguration 来注解相应的测试类。
测试类中 @WebAppConfiguration
的存在指示 TestContext 框架(TCF)应该为你的集成测试加载一个 WebApplicationContext(WAC)。在后台,TCF 确保一个 MockServletContext 被创建并提供给你的测试的 WAC。默认情况下,MockServletContext 的基本资源路径被设置为 src/main/webapp
。这被解释为相对于你的 JVM 根的路径(通常是你项目的路径)。如果你熟悉 Maven 项目中 Web 应用的目录结构,就会知道 src/main/webapp
是 WAR 根目录的默认位置。如果你需要覆盖这个默认位置,你可以在 @WebAppConfiguration
注解中提供一个替代路径(例如,@WebAppConfiguration("src/test/webapp")
)。如果你希望从 classpath 而不是文件系统中引用基础资源路径,你可以使用 Spring 的 classpath:
前缀。
请注意,Spring 对 WebApplicationContext 实现的测试支持与它对标准 ApplicationContext 实现的支持是一样的。当使用WebApplicationContext 进行测试时,你可以通过使用 @ContextConfiguration
来自由声明 XML 配置文件、Groovy 脚本或@Configuration 类。你也可以自由地使用任何其他的测试注解,如 @ActiveProfiles、@TestExecutionListeners、@Sql、@Rollback,以及其他。
本节剩下的例子展示了一些加载 WebApplicationContext 的各种配置选项。下面的例子展示了 TestContext 框架对惯例高于配置的支持:
@ExtendWith(SpringExtension.class)
// 默认资源路径为 "file:src/main/webapp"
@WebAppConfiguration
// 默认使用测试类同名包下的 WacTests-context.xml 文件加载 应用上下文
// 或则 被 @Configuration 注解的静态类
@ContextConfiguration
class WacTests {
//...
}
如果使用 @WebAppConfiguration 注解测试类而不指定资源基路径,那么资源路径实际上默认为 file:src/main/webapp
。类似地,如果您在声明 @ContextConfiguration 时没有指定资源位置、组件类或上下文初始值设定项,Spring 会尝试使用约定(即 WacTests-context.xml 与 WacTests 类或静态嵌套的 @configuration 类位于同一个包中)来检测配置的存在。
以下示例显示了如何使用 @WebAppConfiguration 显式声明资源基路径,以及使用 @ContextConfiguration 显式声明XML资源位置:
@ExtendWith(SpringExtension.class)
// file system resource
@WebAppConfiguration("webapp")
// classpath resource
@ContextConfiguration("/spring/test-servlet-config.xml")
class WacTests {
//...
}
这里需要注意的重要一点是,使用这两个注解的路径具有不同的语义。默认情况下,@WebAppConfiguration 资源路径基于文件系统,而@ContextConfiguration 资源位置基于类路径。
以下示例显示,我们可以通过指定 Spring 资源前缀 来覆盖这两个注解的默认资源语义:
@ExtendWith(SpringExtension.class)
// classpath resource
@WebAppConfiguration("classpath:test-web-resources")
// file system resource
@ContextConfiguration("file:src/main/webapp/WEB-INF/servlet-config.xml")
class WacTests {
//...
}
将本例中的注解与上一个示例中的注解进行对比。
与 Web Mocks 一起工作
为了提供全面的 Web 测试支持,TestContext 框架有一个 ServletTestExecutionListener,默认是启用的。当针对 WebApplicationContext 进行测试时,这个 TestExecutionListener 通过在每个测试方法之前使用 Spring Web 的 RequestContextHolder 来设置默认的线程本地状态,并根据用 @WebAppConfiguration 配置的基本资源路径创建一个 MockHttpServletRequest、一个 MockHttpServletResponse 和一个 ServletWebRequest。ServletTestExecutionListener 还确保 MockHttpServletResponse 和 ServletWebRequest 可以被注入到测试实例中,并且,一旦测试完成,它将清理线程本地状态。
一旦你为你的测试加载了一个 WebApplicationContext,你可能会发现你需要与 Web 模拟进行交互,例如,设置你的测试夹具或在调用你的 Web 组件后执行断言。下面的例子显示了哪些 mock 可以被自动连接到你的测试实例。注意,WebApplicationContext 和 MockServletContext 都在整个测试套件中被缓存,而其他 mock 则由 ServletTestExecutionListener 管理每个测试方法:
@SpringJUnitWebConfig
class WacTests {
@Autowired
WebApplicationContext wac; // 缓存
@Autowired
MockServletContext servletContext; // 缓存
@Autowired
MockHttpSession session;
@Autowired
MockHttpServletRequest request;
@Autowired
MockHttpServletResponse response;
@Autowired
ServletWebRequest webRequest;
//...
}
上下文缓存
一旦 TestContext 框架为一个测试加载了 ApplicationContext(或 WebApplicationContext),该上下文就会被缓存,并在同一测试套件中声明相同的唯一上下文配置的所有后续测试中重复使用。要理解缓存是如何工作的,重要的是要理解 唯一 和 测试套件 的含义。
一个 ApplicationContext 可以通过用于加载它的配置参数的组合来唯一地识别。因此,配置参数的独特组合被用来生成一个 key,在这个 key下,上下文被缓存。TestContext 框架使用以下配置参数来建立上下文缓存 key:
- locations (from @ContextConfiguration)
- classes (from @ContextConfiguration)
- contextInitializerClasses (from @ContextConfiguration)
- contextCustomizers (from ContextCustomizerFactory) –这包括 @DynamicPropertySource 方法,以及 Spring Boot 测试支持中的各种功能,如 @MockBean 和 @SpyBean。
- contextLoader (from @ContextConfiguration)
- parent (from @ContextHierarchy)
- activeProfiles (from @ActiveProfiles)
- propertySourceLocations (from @TestPropertySource)
- propertySourceProperties (from @TestPropertySource)
- resourceBasePath (from @WebAppConfiguration)
例如,如果 TestClassA 为 @ContextConfiguration 的l ocation(或 value)属性指定了 {"app-config.xml", "test-config.xml"}
,TestContext 框架就会加载相应的 ApplicationContext 并将其存储在静态上下文缓存中,该缓存的键只基于这些位置。因此,如果 TestClassB 也定义了 {"app-config.xml", "test-config.xml"}
作为它的位置(无论是显式还是隐式通过继承),但没有定义@WebAppConfiguration、不同的 ContextLoader、不同的活动配置文件、不同的上下文初始化器、不同的测试属性源或不同的父级上下文,那么两个测试类就共享同一个 ApplicationContext。这意味着加载应用程序上下文的设置成本只发生一次(每个测试套件),随后的测试执行会快很多。
:::info 测试套件和 forked 进程
Spring TestContext 框架在静态缓存中存储应用程序上下文。这意味着上下文实际上是存储在一个静态变量中。换句话说,如果测试在不同的进程中运行,静态缓存在每次测试执行之间被清除,这实际上是禁用了缓存机制。
为了从缓存机制中获益,所有的测试必须在同一进程或测试套件中运行。这可以通过在 IDE 中作为一个组执行所有的测试来实现。同样,当用 Ant、Maven 或 Gradle 等构建框架执行测试时,必须确保构建框架在测试之间不分叉。例如,如果 Maven Surefire 插件的 forkMode 被设置为 always 或 pertest,TestContext 框架就不能在测试类之间缓存应用上下文,构建过程的运行速度会因此而大大降低。 :::
上下文缓存的大小是有限制的,默认最大大小为 32。每当达到最大容量时,就会使用最近使用最少的驱逐策略(LRU)来驱逐和关闭陈旧的上下文。你可以通过设置名为 spring.test.context.cache.maxSize
的 JVM 系统属性,从命令行或构建脚本中配置最大尺寸。作为替代方法,你可以通过 SpringProperties 机制设置相同的属性。
由于在一个给定的测试套件中加载大量的应用程序上下文会导致套件运行时间过长,因此准确了解有多少上下文被加载和缓存往往是有益的。要查看底层上下文缓存的统计数据,你可以将 org.springframework.test.context.cache
日志类别的日志级别设为 DEBUG。
在不太可能的情况下,如果测试破坏了应用程序上下文并需要重新加载(例如,通过修改 Bean 定义或应用程序对象的状态),你可以用@DirtiesContext 来注解你的测试类或测试方法(见 Spring 测试注释中关于 @DirtiesContext 的讨论)。这指示 Spring 从缓存中删除上下文,并在运行需要相同应用上下文的下一个测试之前重建应用上下文。请注意,对 @DirtiesContext 注解的支持是由DirtiesContextBeforeModesTestExecutionListener 和 DirtiesContextTestExecutionListener 提供的,它们默认是启用的。
:::info ApplicationContext 生命周期和控制台记录
当你需要调试一个用 Spring TestContext 框架执行的测试时,分析控制台输出(即输出到 SYSOUT 和 SYSERR 流)是很有用的。一些构建工具和 IDE 能够将控制台输出与给定的测试相关联;然而,一些控制台输出不能轻易与给定的测试相关联。
关于由 Spring 框架本身或在 ApplicationContext 中注册的组件触发的控制台日志,重要的是了解在测试套件中由 Spring TestContext 框架加载的 ApplicationContext 的生命周期。
测试的 ApplicationContext 通常在准备测试类的实例时被加载—例如,对测试实例的 @Autowired 字段进行依赖注入。这意味着在 ApplicationContext 的初始化过程中触发的任何控制台日志通常不能与单个测试方法相关。然而,如果根据 @DirtiesContext 语义,在执行测试方法之前立即关闭上下文,那么在执行测试方法之前,将加载一个新的上下文实例。在后一种情况下,IDE 或构建工具有可能将控制台记录与单个测试方法联系起来。 :::
上下文层次结构
当编写依赖于加载的 Spring ApplicationContext 的集成测试时,针对单个上下文进行测试通常是足够的。然而,有些时候,针对ApplicationContext 实例的层次结构进行测试是有益的,甚至是必要的。例如,如果你正在开发一个 Spring MVC Web 应用程序,你通常有一个由 Spring 的 ContextLoaderListener 加载的根 WebApplicationContext 和一个由 Spring 的 DispatcherServlet 加载的子 WebApplicationContext。这导致了一个父子上下文层次结构,其中共享组件和基础设施配置在根上下文中被声明,并在子上下文中被特定的 Web 组件消耗。另一个用例可以在 Spring Batch 应用程序中找到,在那里,你通常有一个为共享批处理基础设施提供配置的父上下文和一个为特定批处理作业配置的子上下文。
你可以通过使用 @ContextHierarchy 注解声明上下文配置来编写使用上下文分层的集成测试,无论是在单个测试类上还是在测试类分层中。如果在一个测试类层次结构中的多个类上声明了上下文层次结构,你也可以合并或覆盖上下文层次结构中特定的、命名的级别的上下文配置。当合并层次结构中特定级别的配置时,配置资源类型(即 XML 配置文件或组件类)必须一致。否则,完全可以接受上下文层次结构中的不同级别使用不同的资源类型进行配置。
本节中剩余的基于 JUnit Jupiter 的例子显示了需要使用上下文层次结构的集成测试的常见配置情况。
具有上下文层次结构的单一测试类
ControllerIntegrationTests 代表了一个典型的 Spring MVC Web 应用程序的集成测试场景,它声明了一个由两个层次组成的上下文层次,一个是根 WebApplicationContext(通过使用 TestAppConfig @Configuration 类加载),一个是调度器 Servlet WebApplicationContext(通过使用 WebConfig @Configuration 类加载)。被自动连接到测试实例的 WebApplicationContext 是子上下文(即层次结构中最低的上下文)。下面的列表显示了这种配置情况:
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextHierarchy({
@ContextConfiguration(classes = TestAppConfig.class),
@ContextConfiguration(classes = WebConfig.class)
})
class ControllerIntegrationTests {
@Autowired
WebApplicationContext wac;
// ...
}
具有隐性父级上下文的类层次结构
本例中的测试类在测试类层次结构中定义了一个上下文层次结构。AbstractWebTests 声明了 Spring 驱动的 Web 应用中根 WebApplicationContext 的配置。但是请注意,AbstractWebTests 并没有声明 @ContextHierarchy。因此,AbstractWebTests 的子类可以选择性地参与上下文层次结构,或遵循 @ContextConfiguration 的标准语义。SoapWebServiceTests 和 RestWebServiceTests 都扩展了AbstractWebTests,并通过使用 @ContextHierarchy 定义了一个上下文层次结构。其结果是,三个应用程序上下文被加载(每个 @ContextConfiguration 的声明都有一个),基于 AbstractWebTests 中的配置加载的应用程序上下文被设置为具体子类加载的每个上下文的父级上下文。下面的列表显示了这种配置情况。
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public abstract class AbstractWebTests {}
@ContextHierarchy(@ContextConfiguration("/spring/soap-ws-config.xml"))
public class SoapWebServiceTests extends AbstractWebTests {}
@ContextHierarchy(@ContextConfiguration("/spring/rest-ws-config.xml"))
public class RestWebServiceTests extends AbstractWebTests {}
带有合并的上下文层次配置的类层次结构
这个例子中的类展示了命名的层次结构级别的使用,以便合并上下文层次结构中特定级别的配置。BaseTests 在层次结构中定义了两个层次,父级和子级。ExtendedTests 扩展了 BaseTests,并指示 Spring TestContext Framework 合并子层级的上下文配置,方法是确保 @ContextConfiguration 中的 name 属性中声明的名字都是子层级。结果是三个应用上下文被加载:一个是 /app-config.xml,一个是 /user-config.xml,还有一个是 {"/user-config.xml", "/order-config.xml"}
。与前面的例子一样,从 /app-config.xml 加载的应用程序上下文被设置为从 /user-config.xml
和 {"/user-config.xml", "/order-config.xml"}
加载的上下文的父级上下文。下面的列表显示了这种配置情况。
@ExtendWith(SpringExtension.class)
@ContextHierarchy({
@ContextConfiguration(name = "parent", locations = "/app-config.xml"),
@ContextConfiguration(name = "child", locations = "/user-config.xml")
})
class BaseTests {}
@ContextHierarchy(
@ContextConfiguration(name = "child", locations = "/order-config.xml")
)
class ExtendedTests extends BaseTests {}
带有被覆盖的上下文层次结构配置的类层次结构
与前面的例子相反,这个例子演示了如何通过将 @ContextConfiguration 中的 inheritLocations 标志设置为 false 来覆盖上下文层次结构中某个指定的命名层次的配置。因此,ExtendedTests 的应用上下文只从 /test-user-config.xml 中加载,并且其父级设置为从 /app-config.xml 中加载的上下文。下面的列表显示了这种配置情况。
@ExtendWith(SpringExtension.class)
@ContextHierarchy({
@ContextConfiguration(name = "parent", locations = "/app-config.xml"),
@ContextConfiguration(name = "child", locations = "/user-config.xml")
})
class BaseTests {}
@ContextHierarchy(
@ContextConfiguration(
name = "child",
locations = "/test-user-config.xml",
inheritLocations = false
))
class ExtendedTests extends BaseTests {}
:::info 清理上下文层次结构中的一个上下文
如果你在一个测试中使用 @DirtiesContext,其上下文被配置为上下文层次结构的一部分,你可以使用 hierarchyMode 标志来控制上下文缓存的清除方式。有关进一步的细节,请参阅 Spring Testing Annotations 中关于 @DirtiesContext 的讨论和 @DirtiesContext javadoc。 :::