使用环境配置文件进行上下文配置

TODAY Framework 对环境和配置文件(又名 "bean 定义配置文件")的概念提供了一流的支持,集成测试可以配置为在加载测试的 ApplicationContext 时激活特定的 bean 定义配置文件。这是通过使用 @ActiveProfiles 注解标注测试类并提供应在加载 ApplicationContext 时激活的配置文件列表来实现的。

你可以将 @ActiveProfilesSmartContextLoader SPI 的任何实现一起使用,但 @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(InfraExtension.class)
// ApplicationContext 将从 "classpath:/app-config.xml" 加载
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
class TransferServiceTest {

  @Autowired
  TransferService transferService;

  @Test
  void testTransferService() {
    // 测试 transferService
  }
}

当运行 TransferServiceTest 时,其 ApplicationContext 从类路径根目录下的 app-config.xml 配置文件加载。如果你检查 app-config.xml,你会看到 accountRepository bean 依赖于 dataSource bean。然而,dataSource 并没有定义为顶级 bean。相反,dataSource 被定义了三次:在 production 配置文件中,在 dev 配置文件中,以及在 default 配置文件中。

通过使用 @ActiveProfiles("dev") 标注 TransferServiceTest,我们指示 Infra TestContext 框架在加载 ApplicationContext 时将活动配置文件设置为 {"dev"}。结果是,创建了一个嵌入式数据库并填充了测试数据,并且 accountRepository bean 被装配了对开发 DataSource 的引用。这很可能就是我们在集成测试中想要的。

有时将 bean 分配给 default 配置文件很有用。默认配置文件中的 bean 仅在没有其他配置文件被特别激活时才包含在内。你可以使用它来定义在应用程序默认状态下使用的 “fallback” bean。例如,你可以为 devproduction 配置文件显式提供数据源,但在两者均未激活时定义内存数据源作为默认值。

以下代码清单演示了如何使用 @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();
  }
}
@JUnitConfig({
    TransferServiceConfig.class,
    StandaloneDataConfig.class,
    JndiDataConfig.class,
    DefaultDataConfig.class})
@ActiveProfiles("dev")
class TransferServiceTest {

  @Autowired
  TransferService transferService;

  @Test
  void testTransferService() {
    // 测试 transferService
  }
}

在这个变体中,我们将 XML 配置拆分为四个独立的 @Configuration 类:

  • TransferServiceConfig: 通过使用 @Autowired 进行依赖注入来获取 dataSource

  • StandaloneDataConfig: 为适合开发人员测试的嵌入式数据库定义 dataSource

  • JndiDataConfig: 定义从生产环境中的 JNDI 检索的 dataSource

  • DefaultDataConfig: 为默认嵌入式数据库定义 dataSource,以防没有配置文件处于活动状态。

与基于 XML 的配置示例一样,我们仍然使用 @ActiveProfiles("dev") 标注 TransferServiceTest,但这次我们使用 @ContextConfiguration 注解指定所有四个配置类。测试类本身的主体完全保持不变。

通常情况下,在给定项目中的多个测试类中使用同一组配置文件。因此,为了避免重复声明 @ActiveProfiles 注解,你可以将 @ActiveProfiles 声明在基类上一次,子类会自动从基类继承 @ActiveProfiles 配置。在以下示例中,@ActiveProfiles 的声明(以及其他注解)已移动到抽象超类 AbstractIntegrationTest 中:

自 TODAY Framework 5.3 起,测试配置也可以从封闭类继承。有关详细信息,请参阅 @Nested 测试类配置
@JUnitConfig({
    TransferServiceConfig.class,
    StandaloneDataConfig.class,
    JndiDataConfig.class,
    DefaultDataConfig.class})
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
// "dev" 配置文件从超类继承
class TransferServiceTest extends AbstractIntegrationTest {

  @Autowired
  TransferService transferService;

  @Test
  void testTransferService() {
    // 测试 transferService
  }
}

@ActiveProfiles 还支持 inheritProfiles 属性,可用于禁用活动配置文件的继承,如以下示例所示:

// "dev" 配置文件被 "production" 覆盖
@ActiveProfiles(profiles = "production", inheritProfiles = false)
class ProductionTransferServiceTest extends AbstractIntegrationTest {
  // 测试主体
}

此外,有时需要以编程方式而不是声明方式解析测试的活动配置文件——例如,基于:

  • 当前操作系统。

  • 测试是否正在持续集成构建服务器上运行。

  • 某些环境变量的存在。

  • 自定义类级注解的存在。

  • 其他关注点。

要以编程方式解析活动 bean 定义配置文件,你可以实现自定义 ActiveProfilesResolver 并使用 @ActiveProfilesresolver 属性注册它。有关更多信息,请参阅相应的 javadoc。 以下示例演示了如何实现和注册自定义 OperatingSystemActiveProfilesResolver

// 通过自定义解析器以编程方式覆盖 "dev" 配置文件
@ActiveProfiles(
    resolver = OperatingSystemActiveProfilesResolver.class,
    inheritProfiles = false)
class TransferServiceTest extends AbstractIntegrationTest {
  // 测试主体
}
public class OperatingSystemActiveProfilesResolver implements ActiveProfilesResolver {

  @Override
  public String[] resolve(Class<?> testClass) {
    String profile = ...;
    // 根据操作系统确定 profile 的值
    return new String[] {profile};
  }
}