Context Configuration with Environment Profiles

The TODAY 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 @ActiveProfiles with any implementation of the SmartContextLoader SPI, but @ActiveProfiles is not supported with implementations of the older ContextLoader SPI.

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>
@ExtendWith(InfraExtension.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
  }
}

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.

By annotating TransferServiceTest with @ActiveProfiles("dev"), we instruct the Infra 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.

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.

The following code listings demonstrate how to implement the same configuration and integration test with @Configuration classes instead of 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() {
    // test the transferService
  }
}

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.

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.

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:

As of TODAY Framework 5.3, test configuration may also be inherited from enclosing classes. See @Nested test class configuration for details.
@JUnitConfig({
    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 also supports an inheritProfiles attribute that can be used to disable the inheritance of active profiles, as the following example shows:

// "dev" profile overridden with "production"
@ActiveProfiles(profiles = "production", inheritProfiles = false)
class ProductionTransferServiceTest extends 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.

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:

// "dev" profile overridden programmatically via a custom resolver
@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 = ...;
    // determine the value of profile based on the operating system
    return new String[] {profile};
  }
}