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.

您可以将 @ActiveProfilesSmartContextLoader SPI 的任何实现结合使用,但 @ActiveProfiles 不受较老 ContextLoader SPI 实现的支持。

You can use @ActiveProfiles with any implementation of the SmartContextLoader SPI, but @ActiveProfiles is not supported with implementations of the older ContextLoader SPI.

考虑两个带有 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。例如,你可以明确地为 devproduction 配置文件提供一个数据源,但在这两个配置文件都不激活时,将其定义为一个内存数据源。

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 a dataSource through dependency injection by using @Autowired.

  • StandaloneDataConfig: Defines a dataSource for an embedded database suitable for developer tests.

  • JndiDataConfig: Defines a dataSource that is retrieved from JNDI in a production environment.

  • DefaultDataConfig: Defines a dataSource 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 开始,还可以从封闭类继承测试配置。请参阅 @Nested test class configuration 了解详情。

As of Spring Framework 5.3, test configuration may also be inherited from enclosing classes. See @Nested test class configuration for details.

  • 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 并使用 @ActiveProfilesresolver 属性对其进行注册。有关更多信息,请参阅相应的 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)
	}
}