Интеграционное тестирование в Spring: TestContext Framework, конфигурация контекста с профилями среды

Spring Framework имеет первоклассную поддержку понятий сред и профилей (также известных как "профили определения bean-компонентов"), а интеграционные тесты можно настроить для активации определенных профилей определения bean-компонентов для различных сценариев тестирования. Это достигается путем аннотирования тестового класса аннотацией @ActiveProfiles и предоставления списка профилей, которые должны быть активированы при загрузке ApplicationContext для теста.

Вы можете использовать @ActiveProfiles с любой реализацией SPI SmartContextLoader, но @ActiveProfiles не поддерживается с реализациями более старого SPI ContextLoader.

Рассмотрим два примера с конфигурацией 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(SpringExtension.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, вы увидите, что bean-компонент accountRepository имеет зависимость от bean-компонента dataSource. Однако dataSource не определен как bean-компонент верхнего уровня. Вместо этого dataSource определяется трижды: в профиле production, в профиле dev и в профиле по умолчанию (default).

Аннотируя TransferServiceTest с помощью @ActiveProfiles("dev"), мы инструктируем Spring TestContext Framework загрузить ApplicationContext с активными профилями, установленными на {"dev"}. В результате создается встроенная база данных, которая заполняется тестовыми данными, а компонент accountRepository связан со ссылкой на источник данных разработки (development DataSource). Вероятно, это то, что мы хотим в интеграционном тесте.

Иногда бывает полезно назначить bean-компоненты профилю по умолчанию. Компоненты в профиле по умолчанию включаются только тогда, когда никакой другой профиль специально не активирован. Вы можете использовать это для определения "резервных" bean-компонентов, которые будут использоваться в состоянии приложения по умолчанию. Например, вы можете явно указать источник данных для профилей dev и production, но определить источник данных в памяти по умолчанию, если ни один из них не активен.

В следующих листингах кода показано, как реализовать те же тесты конфигурации и интеграции с классами @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();
    }
}

@SpringJUnitConfig({
        TransferServiceConfig.class,
        StandaloneDataConfig.class,
        JndiDataConfig.class,
        DefaultDataConfig.class})
@ActiveProfiles("dev")
class TransferServiceTest {

    @Autowired
    TransferService transferService;

    @Test
    void testTransferService() {
        // тестируем transferService
    }
}

В этом варианте мы разделили конфигурацию XML на четыре независимых класса @Configuration:

  • TransferServiceConfig: получает dataSource посредством внедрения зависимостей с помощью @Autowired.
  • StandaloneDataConfig: определяет dataSource для встроенной базы данных, подходящий для тестов разработчика.
  • JndiDataConfig: определяет dataSource, который извлекается из JNDI в производственной среде.
  • DefaultDataConfig: определяет dataSource для встроенной базы данных по умолчанию, если профиль не активен.

Как и в случае с примером конфигурации на основе XML, мы по-прежнему аннотируем TransferServiceTest с помощью @ActiveProfiles("dev"), но на этот раз мы указываем все четыре класса конфигурации с помощью аннотации @ContextConfiguration. Само тело тестового класса полностью не изменилось.

Часто один набор профилей используется в нескольких тестовых классах в рамках одного проекта. Таким образом, чтобы избежать дублирования объявлений аннотации @ActiveProfiles, вы можете один раз объявить @ActiveProfiles в базовом классе, и подклассы автоматически наследуют конфигурацию @ActiveProfiles от базового класса. В следующем примере объявление @ActiveProfiles (а также других аннотаций) перемещено в абстрактный суперкласс, AbstractIntegrationTest:

@SpringJUnitConfig({
        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 и зарегистрировать его с помощью атрибута resolver @ActiveProfiles. В следующем примере показано, как реализовать и зарегистрировать пользовательский 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 = ...;
        // определяем значение профиля в зависимости от операционной системы
        return new String[] {profile};
    }
}


Читайте также:


Комментарии

Популярные сообщения из этого блога

Методы класса Object в Java

Как получить текущий timestamp в Java

Основные опции JVM для повышения производительности и отладки