使用环境配置文件进行上下文配置
Spring框架对环境(environment)和配置文件(profile,也称为“bean定义配置文件”)的概念提供了一流的支持,集成测试可以配置为在各种测试场景下激活特定的bean定义配置文件。这是通过使用@ActiveProfiles注解来标注测试类,并提供在加载测试的ApplicationContext时应该激活的配置文件列表来实现的。
你可以将 @ActiveProfiles 与任何实现 SmartContextLoader SPI 的类一起使用,但较旧的 ContextLoader SPI 实现不支持 @ActiveProfiles。
考虑两个使用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>
- 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(即没有在<bean>标签中直接定义)。实际上,dataSource被定义了三次:分别在production配置文件中、dev配置文件中以及default配置文件中。
通过使用@ActiveProfiles("dev")来标注TransferServiceTest,我们指示Spring TestContext框架加载设置为{"dev"}的活跃配置文件的ApplicationContext。因此,会创建一个嵌入式数据库并填充测试数据,同时accountRepositorybean也会被关联到开发环境的DataSource。这很可能正是我们在集成测试中所需要的。
有时将Bean分配给default配置文件会很有用。只有当没有其他配置文件被特别激活时,default配置文件中的Bean才会被包含进来。你可以利用这一点来定义应用程序默认状态下使用的“备用”Bean。例如,你可能会为dev和production配置文件明确提供数据源,但当这两个配置文件都不处于激活状态时,你可以定义一个内存中的数据源作为默认数据源。
以下代码示例展示了如何使用@Configuration类来实现与使用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类:
TransferServiceConfig: 通过使用@Autowired通过依赖注入获取dataSource。StandaloneDataConfig: 定义一个适用于开发者测试的嵌入式数据库的dataSource。JndiDataConfig: 定义一个在生产环境中从 JNDI 中获取的dataSource。DefaultDataConfig: 定义一个默认嵌入式数据库的dataSource,以防没有激活任何配置文件。
与基于XML的配置示例一样,我们仍然使用@ActiveProfiles("dev")来注释TransferServiceTest,但这次我们通过使用@ContextConfiguration注解来指定全部四个配置类。测试类本身的内容则完全保持不变。
通常情况下,一个项目中多个测试类会使用同一组配置文件(profiles)。因此,为了避免重复声明@ActiveProfiles注解,你可以在基类上只声明一次@ActiveProfiles,子类就会自动从基类继承该配置。在以下示例中,@ActiveProfiles的声明(以及其他注解的声明)被移至了一个抽象超类AbstractIntegrationTest中:
测试配置也可以从包含类中继承。详情请参见@嵌套测试类配置。
- 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 属性,该属性可用于禁用活动配置文件的继承,如下例所示:
- 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
}
此外,有时需要通过编程方式而不是声明式方式来解析活动配置文件进行测试——例如,基于以下情况:
- 当前的操作系统。
- 测试是否在持续集成构建服务器上运行。
- 某些环境变量的存在。
- 自定义类级注解的存在。
- 其他相关因素。
要编程方式地解决活跃Bean定义配置文件的问题,你可以实现一个自定义的ActiveProfilesResolver,并通过使用@ActiveProfiles的resolver属性来注册它。有关更多信息,请参阅相应的javadoc。以下示例演示了如何实现和注册一个自定义的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)
}
}