Context Configuration with Environment Profiles
Spring 框架对环境和模式(又名 “bean 定义模式”)的概念提供了一流的支持,可以将集成测试配置为针对不同的测试场景激活特定的 bean 定义模式。这可以通过用 @ActiveProfiles
注释测试类并提供应在加载测试的 ApplicationContext
时激活的模式列表来实现。
The Spring Framework has first-class support for the notion of environments and profiles
(AKA "bean definition profiles"), and integration tests can be configured to activate
particular bean definition profiles for various testing scenarios. This is achieved by
annotating a test class with the @ActiveProfiles
annotation and supplying a list of
profiles that should be activated when loading the ApplicationContext
for the test.
您可以将 |
You can use |
考虑两个带有 XML 配置和 @Configuration
类的示例:
Consider two examples with XML configuration and @Configuration
classes:
<!-- 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>
-
Java
-
Kotlin
@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
}
}
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from "classpath:/app-config.xml"
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
class TransferServiceTest {
@Autowired
lateinit var transferService: TransferService
@Test
fun testTransferService() {
// test the transferService
}
}
当运行 TransferServiceTest
时,它的 ApplicationContext
从类路径根目录的 app-config.xml
配置文件中加载。如果你检查 app-config.xml
,你就会看到 accountRepository
bean 对一个 dataSource
bean 有依赖。然而,dataSource
没有定义为顶级 bean。相反,dataSource
定义了三次:在 production
模式中,在 dev
模式中和在 default
模式中。
When TransferServiceTest
is run, its ApplicationContext
is loaded from the
app-config.xml
configuration file in the root of the classpath. If you inspect
app-config.xml
, you can see that the accountRepository
bean has a dependency on a
dataSource
bean. However, dataSource
is not defined as a top-level bean. Instead,
dataSource
is defined three times: in the production
profile, in the dev
profile,
and in the default
profile.
通过用 @ActiveProfiles("dev")
为 TransferServiceTest
加注解,我们指示 SpringTestContext Framework
使用 {"dev"}
设置激活的配置文件,加载 ApplicationContext
。这样做创建了一个嵌入式数据库,里面填充了测试数据,并且 accountRepository
bean 用对开发 DataSource
的引用连接起来。这很可能是我们在集成测试中想要的结果。
By annotating TransferServiceTest
with @ActiveProfiles("dev")
, we instruct the Spring
TestContext Framework to load the ApplicationContext
with the active profiles set to
{"dev"}
. As a result, an embedded database is created and populated with test data, and
the accountRepository
bean is wired with a reference to the development DataSource
.
That is likely what we want in an integration test.
有时为 default
配置文件分配 bean 很有效。只有在没有特别激活其他配置文件时,才会包含 default
配置文件中的 bean。你可以用它来定义要应用于应用程序默认状态的“fallback
”bean。例如,你可以明确地为 dev
和 production
配置文件提供一个数据源,但在这两个配置文件都不激活时,将其定义为一个内存数据源。
It is sometimes useful to assign beans to a default
profile. Beans within the default
profile are included only when no other profile is specifically activated. You can use
this to define “fallback” beans to be used in the application’s default state. For
example, you may explicitly provide a data source for dev
and production
profiles,
but define an in-memory data source as a default when neither of these is active.
以下代码清单演示了如何使用 @Configuration
类(而不是 XML)实现相同的配置和集成测试:
The following code listings demonstrate how to implement the same configuration and
integration test with @Configuration
classes instead of XML:
-
Java
-
Kotlin
@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("dev")
class StandaloneDataConfig {
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build()
}
}
-
Java
-
Kotlin
@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("production")
class JndiDataConfig {
@Bean(destroyMethod = "")
fun dataSource(): DataSource {
val ctx = InitialContext()
return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
}
}
-
Java
-
Kotlin
@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
@Profile("default")
class DefaultDataConfig {
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.build()
}
}
-
Java
-
Kotlin
@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();
}
}
@Configuration
class TransferServiceConfig {
@Autowired
lateinit var dataSource: DataSource
@Bean
fun transferService(): TransferService {
return DefaultTransferService(accountRepository(), feePolicy())
}
@Bean
fun accountRepository(): AccountRepository {
return JdbcAccountRepository(dataSource)
}
@Bean
fun feePolicy(): FeePolicy {
return ZeroFeePolicy()
}
}
-
Java
-
Kotlin
@SpringJUnitConfig({
TransferServiceConfig.class,
StandaloneDataConfig.class,
JndiDataConfig.class,
DefaultDataConfig.class})
@ActiveProfiles("dev")
class TransferServiceTest {
@Autowired
TransferService transferService;
@Test
void testTransferService() {
// test the transferService
}
}
@SpringJUnitConfig(
TransferServiceConfig::class,
StandaloneDataConfig::class,
JndiDataConfig::class,
DefaultDataConfig::class)
@ActiveProfiles("dev")
class TransferServiceTest {
@Autowired
lateinit var transferService: TransferService
@Test
fun testTransferService() {
// test the transferService
}
}
在这个变体中,我们把 XML 配置拆分为四个独立的 @Configuration
类:
In this variation, we have split the XML configuration into four independent
@Configuration
classes:
-
TransferServiceConfig
: Acquires adataSource
through dependency injection by using@Autowired
. -
StandaloneDataConfig
: Defines adataSource
for an embedded database suitable for developer tests. -
JndiDataConfig
: Defines adataSource
that is retrieved from JNDI in a production environment. -
DefaultDataConfig
: Defines adataSource
for a default embedded database, in case no profile is active.
和基于 XML 的配置示例一样,我们仍然用 @ActiveProfiles("dev")
为 TransferServiceTest
加注解,但这一次,我们通过使用 @ContextConfiguration
注解指定全部四个配置文件。测试类本身的主体保持完全不变。
As with the XML-based configuration example, we still annotate TransferServiceTest
with
@ActiveProfiles("dev")
, but this time we specify all four configuration classes by
using the @ContextConfiguration
annotation. The body of the test class itself remains
completely unchanged.
通常情况下,在一个给定项目的多项测试中,使用一套配置文件。因此,为了避免重复声明 @ActiveProfiles
注解,你可以在一个基类上声明一次 @ActiveProfiles
,并且子类会自动从基类继承 @ActiveProfiles
配置。在以下示例中,@ActiveProfiles
的声明(以及其他注解)已移至一个抽象超类 AbstractIntegrationTest
:
It is often the case that a single set of profiles is used across multiple test classes
within a given project. Thus, to avoid duplicate declarations of the @ActiveProfiles
annotation, you can declare @ActiveProfiles
once on a base class, and subclasses
automatically inherit the @ActiveProfiles
configuration from the base class. In the
following example, the declaration of @ActiveProfiles
(as well as other annotations)
has been moved to an abstract superclass, AbstractIntegrationTest
:
从 Spring Framework 5.3 开始,还可以从封闭类继承测试配置。请参阅 |
As of Spring Framework 5.3, test configuration may also be inherited from enclosing
classes. See |
-
Java
-
Kotlin
@SpringJUnitConfig({
TransferServiceConfig.class,
StandaloneDataConfig.class,
JndiDataConfig.class,
DefaultDataConfig.class})
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
@SpringJUnitConfig(
TransferServiceConfig::class,
StandaloneDataConfig::class,
JndiDataConfig::class,
DefaultDataConfig::class)
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
-
Java
-
Kotlin
// "dev" profile inherited from superclass
class TransferServiceTest extends AbstractIntegrationTest {
@Autowired
TransferService transferService;
@Test
void testTransferService() {
// test the transferService
}
}
// "dev" profile inherited from superclass
class TransferServiceTest : AbstractIntegrationTest() {
@Autowired
lateinit var transferService: TransferService
@Test
fun testTransferService() {
// test the transferService
}
}
@ActiveProfiles
同时支持 inheritProfiles
属性,可以使用它来禁用激活配置文件的继承,如下例所示:
@ActiveProfiles
also supports an inheritProfiles
attribute that can be used to
disable the inheritance of active profiles, as the following example shows:
-
Java
-
Kotlin
// "dev" profile overridden with "production"
@ActiveProfiles(profiles = "production", inheritProfiles = false)
class ProductionTransferServiceTest extends AbstractIntegrationTest {
// test body
}
// "dev" profile overridden with "production"
@ActiveProfiles("production", inheritProfiles = false)
class ProductionTransferServiceTest : AbstractIntegrationTest() {
// test body
}
此外,有时需要以编程方式而不是声明性地解析用于测试的激活配置文件,例如,根据:
Furthermore, it is sometimes necessary to resolve active profiles for tests programmatically instead of declaratively — for example, based on:
-
The current operating system.
-
Whether tests are being run on a continuous integration build server.
-
The presence of certain environment variables.
-
The presence of custom class-level annotations.
-
Other concerns.
要以编程方式解析活动 bean 定义配置文件,你可以实现一个自定义 ActiveProfilesResolver
并使用 @ActiveProfiles
的 resolver
属性对其进行注册。有关更多信息,请参阅相应的 javadoc。以下示例演示如何实现和注册一个自定义 OperatingSystemActiveProfilesResolver
:
To resolve active bean definition profiles programmatically, you can implement
a custom ActiveProfilesResolver
and register it by using the resolver
attribute of @ActiveProfiles
. For further information, see the corresponding
javadoc.
The following example demonstrates how to implement and register a custom
OperatingSystemActiveProfilesResolver
:
-
Java
-
Kotlin
// "dev" profile overridden programmatically via a custom resolver
@ActiveProfiles(
resolver = OperatingSystemActiveProfilesResolver.class,
inheritProfiles = false)
class TransferServiceTest extends AbstractIntegrationTest {
// test body
}
// "dev" profile overridden programmatically via a custom resolver
@ActiveProfiles(
resolver = OperatingSystemActiveProfilesResolver::class,
inheritProfiles = false)
class TransferServiceTest : AbstractIntegrationTest() {
// test body
}
-
Java
-
Kotlin
public class OperatingSystemActiveProfilesResolver implements ActiveProfilesResolver {
@Override
public String[] resolve(Class<?> testClass) {
String profile = ...;
// determine the value of profile based on the operating system
return new String[] {profile};
}
}
class OperatingSystemActiveProfilesResolver : ActiveProfilesResolver {
override fun resolve(testClass: Class<*>): Array<String> {
val profile: String = ...
// determine the value of profile based on the operating system
return arrayOf(profile)
}
}