Каждый TestContext обеспечивает управление контекстом и поддержку кэширования для экземпляра теста, за который он отвечает. Тестовые экземпляры не получают доступ к настроенному ApplicationContext автоматически. Однако, если тестовый класс реализует интерфейс ApplicationContextAware, то ссылка на ApplicationContext передается тестовому экземпляру. Обратите внимание, что AbstractJUnit4SpringContextTests и AbstractTestNGSpringContextTests реализуют ApplicationContextAware и, следовательно, обеспечивают доступ к ApplicationContext автоматически.

ApplicationContext, помеченный аннотацией @Autowired

В качестве альтернативы реализации интерфейса ApplicationContextAware можно внедрить контекст приложения для вашего тестового класса через аннотацию @Autowired для поля или сеттера, как показано в следующем примере:

Java
@SpringJUnitConfig
class MyTest {
    @Autowired
    ApplicationContext applicationContext;
    // тело класса...
}
  1. Внедряем ApplicationContext.
Kotlin
@SpringJUnitConfig
class MyTest {
    @Autowired
    lateinit var applicationContext: ApplicationContext
    // тело класса...
}
  1. Внедряем ApplicationContext.

Аналогично, если ваш тест настроен на загрузку WebApplicationContext, то можно внедрить контекст веб-приложения в тест следующим образом:

Java
@SpringJUnitWebConfig
class MyWebAppTest {
    @Autowired
    WebApplicationContext wac;
    // тело класса...
}
  1. Конфигурируем WebApplicationContext.
  2. Внедряем WebApplicationContext.
Kotlin
@SpringJUnitWebConfig
class MyWebAppTest {
    @Autowired
    lateinit var wac: WebApplicationContext
    // тело класса...
}
  1. Конфигурируем WebApplicationContext.
  2. Внедряем WebApplicationContext.

Внедрение зависимостей с помощью аннотации @Autowired обеспечивается слушателем DependencyInjectionTestExecutionListener, который сконфигурирован по умолчанию (см. "Внедрение зависимостей для тестовых стендов").

Тестовым классам, использующим фреймворк TestContext, не нужно расширять какой-либо конкретный класс или реализовывать определенный интерфейс для конфигурирования своего контекста приложения. Вместо этого конфигурация осуществляется путем объявления аннотации @ContextConfiguration на уровне класса. Если тестовый класс явно не объявляет местоположения ресурсов контекста приложения или компонентные классы, сконфигурированный загрузчик ContextLoader определяет, как загрузить контекст из местоположения по умолчанию или конфигурационных классов по умолчанию. В дополнение к местоположениям ресурсов контекста и компонентным классам, контекст приложения также может быть сконфигурирован с помощью инициализаторов контекста приложения.

В следующих разделах объясняется, как использовать аннотацию @ContextConfiguration в Spring для настройки тестового ApplicationContext с помощью конфигурационных XML-файлов, скриптов Groovy, компонентных классов (обычно классов с аннотацией @Configuration ) или инициализаторов контекста. Кроме того, можно реализовать и сконфигурировать свой собственный SmartContextLoader для расширенных случаев использования.

Конфигурация контекста с использованием ресурсов XML

Чтобы загрузить ApplicationContext для ваших тестов с помощью конфигурационных XML-файлов, аннотируйте ваш тестовый класс с помощью @ContextConfiguration и сконфигурируйте атрибут locations с помощью массива, который содержит данные о местоположении ресурсов конфигурационных метаданных XML. Обычный или относительный путь (например, context.xml) рассматривается как ресурс classpath, который относится к пакету, в котором определен тестовый класс. Путь, начинающийся с косой черты, считается абсолютным местоположением classpath (например, /org/example/config.xml). Путь, представляющий URL ресурса (т.е. путь с префиксами classpath:, file:, http: и т.д.), используется как есть.

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext будет загружен из "/app-config.xml" и
// "/test-config.xml" в корне пути классов
@ContextConfiguration(locations={"/app-config.xml", "/test-config.xml"})
class MyTest {
    // тело класса...
}
  1. Задаем атрибут местоположений в списке XML-файлов.
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext будет загружен из "/app-config.xml" и
// "/test-config.xml" в корне пути классов
@ContextConfiguration("/app-config.xml", "/test-config.xml")
class MyTest {
    // тело класса...
}
  1. Задаем атрибут местоположений в списке XML-файлов.

Аннотация @ContextConfiguration поддерживает псевдоним для атрибута locations через стандартный атрибут value из Java. Таким образом, если не требуется объявлять дополнительные атрибуты в аннотации @ContextConfiguration, то можно опустить объявление имени атрибута locations и объявить местоположения ресурсов, используя сокращенный формат, продемонстрированный в следующем примере:

Java
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-config.xml"})
class MyTest {
    // тело класса...
}
  1. Задаем XML-файлы без использования атрибута location.
Kotlin
@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-config.xml")
class MyTest {
    // тело класса...
}
  1. Задаем XML-файлы без использования атрибута location.

Если вы опустите атрибуты location и value в аннотации @ContextConfiguration, фреймворк TestContext попытается определить местоположение XML-ресурса по умолчанию. В частности, GenericXmlContextLoader и GenericXmlWebContextLoader определяют местоположение по умолчанию на основе имени тестового класса. Если ваш класс называется com.example.MyTest, GenericXmlContextLoader загружает контекст приложения из "classpath:com/example/MyTest-context.xml". В следующем примере показано, как это сделать:

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext будет загружен из
// "classpath:com/example/MyTest-context.xml"
@ContextConfiguration
class MyTest {
    // тело класса...
}
  1. Загружаем конфигурацию из местоположения по умолчанию.
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext будет загружен из
// "classpath:com/example/MyTest-context.xml"
@ContextConfiguration
class MyTest {
    // тело класса...
}
  1. Загружаем конфигурацию из местоположения по умолчанию.
Конфигурация контекста с использованием сценариев Groovy

Чтобы загрузить ApplicationContext для ваших тестов с помощью скриптов Groovy, использующих Groovy Bean Definition DSL.
Также вы можете аннотировать свой тестовый класс с помощью @ContextConfiguration и сконфигурировать атрибут locations или value с помощью массива, содержащего местоположения ресурсов скриптов Groovy. Семантика поиска ресурсов для скриптов Groovy такая же, как и для конфигурационных XML-файлов.

Активация поддержки скриптов Groovy
Поддержка использования скриптов Groovy для загрузки ApplicationContext в Spring TestContext Framework активируется автоматически, если Groovy находится в classpath.

В следующем примере показано, как задать конфигурационные файлы Groovy:

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext будет загружен из "/AppConfig.groovy" и
// "/TestConfig.groovy" в корне пути классов
@ContextConfiguration({"/AppConfig.groovy", "/TestConfig.Groovy"})
class MyTest {
    // тело класса...
}
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext будет загружен из "/AppConfig.groovy" и
// "/TestConfig.groovy" в корне пути классов
@ContextConfiguration("/AppConfig.groovy", "/TestConfig.Groovy")
class MyTest {
    // тело класса...
}
  1. Specifying the location of Groovy configuration files.

Если вы опустите атрибуты locations и value из аннотации @ContextConfiguration, фреймворк TestContext попытается определить скрипт Groovy по умолчанию. В частности, GenericGroovyXmlContextLoader и GenericGroovyXmlWebContextLoader определяют местоположение по умолчанию на основе имени тестового класса. Если ваш класс имеет имя com.example.MyTest, загрузчик контекста Groovy загрузит контекст вашего приложения из "classpath:com/example/MyTestContext.groovy". В следующем примере показано, как использовать значение по умолчанию:

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext будет загружен из
// "classpath:com/example/MyTestContext.groovy"
@ContextConfiguration
class MyTest {
    // тело класса...
}
  1. Загружаем конфигурацию из местоположения по умолчанию.
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext будет загружен из
// "classpath:com/example/MyTestContext.groovy"
@ContextConfiguration
class MyTest {
    // тело класса...
}
  1. Загружаем конфигурацию из местоположения по умолчанию.
Одновременное объявление XML-конфигурации и скриптов Groovy

Вы можете объявить конфигурационные XML-файлы и скрипты Groovy одновременно, используя атрибут location или value в аннотации @ContextConfiguration. Если путь к местоположению сконфигурированного ресурса заканчивается на .xml, он загружается с помощью XmlBeanDefinitionReader. В противном случае он загружается с помощью GroovyBeanDefinitionReader.

В следующем листинге показано, как объединить оба варианта в интеграционном тесте:

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext будет загружен из
// "/app-config.xml" и "/TestConfig.groovy"
@ContextConfiguration({ "/app-config.xml", "/TestConfig.groovy" })
class MyTest {
    // тело класса...
}
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext будет загружен из
// "/app-config.xml" и "/TestConfig.groovy"
@ContextConfiguration("/app-config.xml", "/TestConfig.groovy")
class MyTest {
    // тело класса...
}
Конфигурация контекста с использованием компонентных классов

Чтобы загрузить ApplicationContext для ваших тестов с помощью компонентных классов (см. раздел "Конфигурация контейнера на базе Java"), можно аннотировать свой тестовый класс с помощью @ContextConfiguration и сконфигурировать атрибут classes с помощью массива, содержащего ссылки на компонентные классы. В следующем примере показано, как это сделать:

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext будет загружен из AppConfig и TestConfig
@ContextConfiguration(classes = {AppConfig.class, TestConfig.class})
class MyTest {
    // тело класса...
}
  1. Задание компонентных классов.
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext будет загружен из AppConfig и TestConfig
@ContextConfiguration(classes = [AppConfig::class, TestConfig::class])
class MyTest {
    // тело класса...
}
  1. Задание компонентных классов.
Компонентные классы

Термин "компонентный класс" может относиться к любому из следующих:

  • Классу, аннотированному с помощью @Configuration.

  • Компоненту (то есть классу, помеченному аннотациями @Component, @Service, @Repository или другими стереотипными аннотациями).

  • Классу, соответствующему стандарту JSR-330, аннотированному с помощью аннотаций javax.inject.

  • Любому классу, содержащему метод, аннотированные @Bean.

  • Любому другому классу, который предназначен для регистрации в качестве компонента Spring (т.е. бин Spring в ApplicationContext), потенциально использующий преимущества автоматического обнаружения и связывания единственного конструктора без использования аннотаций в Spring.

Смотрите javadoc по @Configuration и @Bean для получения дополнительной информации о конфигурации и семантике компонентных классов, обращая особое внимание на рассмотрение аннотирования с помощью @Bean в облегченном режиме.

Если вы опустите атрибут classes в аннотации @ContextConfiguration, фреймворк TestContext попытается определить наличие конфигурационных классов по умолчанию. В частности, загрузчики AnnotationConfigContextLoader и AnnotationConfigWebContextLoader обнаруживают все статические вложенные классы тестового класса, которые соответствуют требованиям к реализации конфигурационного класса, как указано в javadoc по @Configuration. Обратите внимание, что имя конфигурационного класса является произвольным. Кроме того, при желании тестовый класс может содержать более одного статического вложенного конфигурационного класса В следующем примере класс OrderServiceTest объявляет статический вложенный конфигурационный класс с именем Config, который автоматически используется для загрузки ApplicationContext для тестового класса:

Java
@SpringJUnitConfig
// ApplicationContext будет загружен из the
// статический вложенный класс Config
class OrderServiceTest {
    @Configuration
    static class Config {
        // этот бин будет внедрен в класс OrderServiceTest
        @Bean
        OrderService orderService() {
            OrderService orderService = new OrderServiceImpl();
            // задаем свойства и т.д.
            return orderService;
        }
    }
    @Autowired
    OrderService orderService;
    @Test
    void testOrderService() {
        // тестируем orderService
    }
}
  1. Загрузка информации о конфигурации из вложенного класса Config.
Kotlin
@SpringJUnitConfig
// ApplicationContext будет загружен из the nested Config class
class OrderServiceTest {
    @Autowired
    lateinit var orderService: OrderService
    @Configuration
    class Config {
        // этот бин будет внедрен в класс OrderServiceTest
        @Bean
        fun orderService(): OrderService {
            // задаем свойства и т.д.
            return OrderServiceImpl()
        }
    }
    @Test
    fun testOrderService() {
        // тестируем orderService
    }
}
  1. Загрузка информации о конфигурации из вложенного класса Config.
Комбинирование XML, скриптов Groovy и компонентных классов

Иногда желательно смешивать конфигурационные XML-файлы , скрипты Groovy и компонентные классы (обычно это помеченные аннотацией @Configuration классы), чтобы сконфигурировать ApplicationContext для ваших тестов. Например, если вы используете XML-конфигурацию в производственной среде, то можете решить, что стоит прибегнуть к классам, помеченным аннотацией @Configuration, чтобы сконфигурировать определенные компоненты, управляемые Spring, для ваших тестов, или наоборот.

Более того, некоторые сторонние фреймворки (например, Spring Boot) предоставляют отличные средства поддержки одновременной загрузки ApplicationContext из различных типов ресурсов (например, конфигурационных XML-файлов, скриптов Groovy и классов с аннотацией @Configuration ). Spring Framework исторически не поддерживал это для стандартных развертываний. Следовательно, большинство реализаций SmartContextLoader, которые Spring Framework поставляет в модуле spring-test, поддерживают только один тип ресурсов для каждого тестового контекста. Однако это не означает, что нельзя использовать оба варианта. Исключением из общего правила является то, что GenericGroovyXmlContextLoader и GenericGroovyXmlWebContextLoader поддерживают одновременно конфигурационные XML-файлы и скрипты Groovy. Более того, сторонние фреймворки могут поддерживать объявление как locations, так и classes через аннотацию @ContextConfiguration, а при стандартной поддержке тестирования во фреймворке TestContext у вас есть возможность, описанные ниже.

Если необходимо использовать местоположения ресурсов (например, XML или Groovy) и классы, аннотированные @Configuration, чтобы сконфигурировать ваши тесты, то вам нужно выбрать что-то из них в качестве точки входа, и этот выбранный класс или местоположение должны включать или импортировать другие. Например, в XML или скрипты Groovy можно включить классы, помеченные аннотацией @Configuration, используя сканирование компонентов или определяя их как обычные бины Spring, в то время как в классе, аннотированном @Configuration, можно использовать аннотацию @ImportResource, чтобы импортировать конфигурационные XML-файлы или скрипты Groovy. Обратите внимание, что такая логика работы семантически эквивалентна тому, как вы конфигурируете свое приложение в производственной среде: В производственной конфигурации вы определяете либо набор местоположений ресурсов XML или Groovy, либо набор классов, помеченных аннотацией@Configuration, из которых загружается ваш производственный ApplicationContext, однако у вас все еще имеется возможность включать или импортировать другой тип конфигурации.

Конфигурация контекстов с использованием инициализаторов контекста

Чтобы настроить ApplicationContext для ваших тестов с помощью инициализаторов контекста, аннотируйте ваш тестовый класс с помощью @ContextConfiguration и сконфигурируйте атрибут initializers с помощью массива, содержащего ссылки на классы, реализующие ApplicationContextInitializer. Объявленные инициализаторы контекста затем используются для инициализации ConfigurableApplicationContext, который загружается для ваших тестов. Обратите внимание, что конкретный тип ConfigurableApplicationContext, поддерживаемый каждым объявленным инициализатором, должен быть совместим с типом ApplicationContext, который создает используемый SmartContextLoader (обычно это GenericApplicationContext). Более того, порядок вызова инициализаторов зависит от того, реализуют ли они интерфейс Ordered из Spring или помечены ли они аннотацией @Order или стандартной аннотацией @Priority. В следующем примере показано, как использовать инициализаторы:

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext будет загружен из TestConfig
// и инициализирован с помощью TestAppCtxInitializer
@ContextConfiguration(
    classes = TestConfig.class,
    initializers = TestAppCtxInitializer.class)
class MyTest {
    // тело класса...
}
  1. Задание конфигурации с помощью конфигурационного класса и инициализатора.
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext будет загружен из TestConfig
// и инициализирован с помощью TestAppCtxInitializer
@ContextConfiguration(
        classes = [TestConfig::class],
        initializers = [TestAppCtxInitializer::class])
class MyTest {
    // тело класса...
}
  1. Задание конфигурации с помощью конфигурационного класса и инициализатора.

Также можно полностью опустить объявление конфигурационных XML-файлов, скриптов Groovy или компонентных классов в аннотации @ContextConfiguration и вместо этого объявить только классы ApplicationContextInitializer, которые затем будут отвечать за регистрацию бинов в контексте – например, путем программной загрузки определений бинов из XML-файлов или конфигурационных классов. В следующем примере показано, как это сделать:

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext будет инициализирован с помощью EntireAppInitializer
// который, как предполагается, регистрирует бины в контексте
@ContextConfiguration(initializers = EntireAppInitializer.class)
class MyTest {
    // тело класса...
}
  1. Задание конфигурации с помощью одного лишь инициализатора.
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext будет инициализирован с помощью EntireAppInitializer
// который, как предполагается, регистрирует бины в контексте
@ContextConfiguration(initializers = [EntireAppInitializer::class])
class MyTest {
    // тело класса...
}
  1. Задание конфигурации с помощью одного лишь инициализатора.
Наследование конфигурации контекста

Аннотация @ContextConfiguration поддерживает булевы атрибуты inheritLocations и inheritInitializers, которые обозначают, следует ли наследовать местоположения ресурсов или компонентные классы и инициализаторы контекста, объявленные суперклассами. Значением по умолчанию для обоих флагов является true. Это означает, что тестовый класс наследует местоположения ресурсов или компонентные классы, а также инициализаторы контекста, объявленные любыми суперклассами. В частности, местоположения ресурсов или компонентные классы для тестового класса добавляются к списку местоположений ресурсов или аннотированных классов, объявленных суперклассами. Схожим образом инициализаторы для данного тестового класса добавляются к набору инициализаторов, определенных суперклассами тестов. Таким образом, подклассы могут расширять местоположения ресурсов, компонентные классы или инициализаторы контекста.

Если атрибут inheritLocations или inheritInitializers в аннотации @ContextConfiguratio.n установлен в false, то местоположения ресурсов или компонентные классы и инициализаторы контекста для тестового класса, соответственно, "затеняют" и эффективно заменяют конфигурацию, определенную суперклассами.

Начиная со Spring Framework 5.3, конфигурация тестов может также наследоваться от объемлющих классов. См. подробности в разделе "Конфигурация тестового класса с аннотацией @Nested".

В следующем примере, в котором используются местоположения XML-ресурсов, ApplicationContext для ExtendedTest загружается из base-config.xml и extended-config.xml именно в таком порядке. Поэтому бины, определенные в extended-config.xml, могут переопределять (то есть заменять) бины, определенные в base-config.xml. В следующем примере показано, как один класс может расширять другой и использовать как свой собственный конфигурационный файл, так и конфигурационный файл суперкласса:

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext будет загружен из "/base-config.xml"
// в корне classpath
@ContextConfiguration("/base-config.xml")
class BaseTest {
    // тело класса...
}
// ApplicationContext будет загружен из "/base-config.xml" и
// "/extended-config.xml" в корне classpath
@ContextConfiguration("/extended-config.xml")
class ExtendedTest extends BaseTest {
    // тело класса...
}
  1. Конфигурационный файл, определенный в суперклассе.
  2. Конфигурационный файл, определенный в подклассе.
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext будет загружен из "/base-config.xml"
// в корне classpath
@ContextConfiguration("/base-config.xml")
open class BaseTest {
    // тело класса...
}
// ApplicationContext будет загружен из "/base-config.xml" and
// "/extended-config.xml" в корне classpath
@ContextConfiguration("/extended-config.xml")
class ExtendedTest : BaseTest() {
    // тело класса...
}
  1. Конфигурационный файл, определенный в суперклассе.
  2. Конфигурационный файл, определенный в подклассе.

Точно так же в следующем примере, в котором используются компонентные классы, ApplicationContext для ExtendedTest загружается из классов BaseConfig и ExtendedConfig именно в таком порядке. Поэтому бины, определенные в ExtendedConfig, могут переопределять (то есть заменять) бины, определенные в BaseConfig. В следующем пример показано, как один класс может расширять другой и использовать как свой собственный конфигурационный класс, так и конфигурационный класс суперкласса:

Java
// ApplicationContext будет загружен из BaseConfig
@SpringJUnitConfig(BaseConfig.class)
class BaseTest {
    // тело класса...
}
// ApplicationContext будет загружен из BaseConfig и ExtendedConfig
@SpringJUnitConfig(ExtendedConfig.class)
class ExtendedTest extends BaseTest {
    // тело класса...
}
  1. Конфигурационный класс, определенный в суперклассе.
  2. Конфигурационный класс, определенный в подклассе.
Kotlin
// ApplicationContext будет загружен из BaseConfig
@SpringJUnitConfig(BaseConfig::class)
open class BaseTest {
    // тело класса...
}
// ApplicationContext будет загружен из BaseConfig и ExtendedConfig
@SpringJUnitConfig(ExtendedConfig::class)
class ExtendedTest : BaseTest() {
    // тело класса...
}
  1. Конфигурационный класс, определенный в суперклассе.
  2. Конфигурационный класс, определенный в подклассе.

В следующем примере, в котором используются инициализаторы контекста, ApplicationContext для ExtendedTest инициализируется с помощью BaseInitializer и ExtendedInitializer. Обратите внимание, однако, что порядок вызова инициализаторов зависит от того, реализуют ли они интерфейс Ordered из Spring или помечены ли они аннотацией @Order из Spring или стандартной аннотацией @Priority. В следующем примере показано, как один класс может расширять другой и использовать как свой собственный инициализатор, так и инициализатор суперкласса:

Java
// ApplicationContext будет инициализирован BaseInitializer
@SpringJUnitConfig(initializers = BaseInitializer.class)
class BaseTest {
    // тело класса...
}
// ApplicationContext будет инициализирован BaseInitializer
// и ExtendedInitializer
@SpringJUnitConfig(initializers = ExtendedInitializer.class)
class ExtendedTest extends BaseTest {
    // тело класса...
}
  1. Инициализатор, определенный в суперклассе.
  2. Инициализатор, определенный в подклассе.
Kotlin
// ApplicationContext будет инициализирован BaseInitializer
@SpringJUnitConfig(initializers = [BaseInitializer::class])
open class BaseTest {
    // тело класса...
}
// ApplicationContext будет инициализирован BaseInitializer
// и ExtendedInitializer
@SpringJUnitConfig(initializers = [ExtendedInitializer::class])
class ExtendedTest : BaseTest() {
    // тело класса...
}
  1. Инициализатор, определенный в суперклассе.
  2. Инициализатор, определенный в подклассе.
Конфигурация контекста с использованием профилей окружения

Spring Framework предлагает первоклассную поддержку понятия сред и профилей (они же "профили определения бинов"), а интеграционные тесты можно настроить на активацию определенных профилей определения бинов для различных сценариев тестирования. Это достигается путем аннотирования тестового класса аннотацией @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>
Java
@ExtendWith(SpringExtension.class)
// ApplicationContext будет загружен из "classpath:/app-config.xml"
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
class TransferServiceTest {
    @Autowired
    TransferService transferService;
    @Test
    void testTransferService() {
        // тестируем transferService
    }
}
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext будет загружен из "classpath:/app-config.xml"
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
class TransferServiceTest {
    @Autowired
    lateinit var transferService: TransferService
    @Test
    fun testTransferService() {
        // тестируем transferService
    }
}

Когда выполняется TransferServiceTest, его ApplicationContext загружается из конфигурационного файла app-config.xml в корне пути классов. Если просмотреть app-config.xml, то можно увидеть, что бин AccountRepository имеет зависимость от бина DataSource. Однако dataSource не определен как высокоуровневый бин. Вместо этого dataSource определяется три раза: в профиле production, в профиле dev и в профиле default.

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

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

В следующих листингах кода продемонстрировано, как реализовать ту же конфигурацию и интеграционный тест с помощью классов с аннотацией @Configuration вместо XML:

Java
@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();
    }
}
Kotlin
@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
@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");
    }
}
Kotlin
@Configuration
@Profile("production")
class JndiDataConfig {
    @Bean(destroyMethod = "")
    fun dataSource(): DataSource {
        val ctx = InitialContext()
        return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
    }
}
Java
@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();
    }
}
Kotlin
@Configuration
@Profile("default")
class DefaultDataConfig {
    @Bean
    fun dataSource(): DataSource {
        return EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.HSQL)
                .addScript("classpath:com/bank/config/sql/schema.sql")
                .build()
    }
}
Java
@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();
    }
}
Kotlin
@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
@SpringJUnitConfig({
        TransferServiceConfig.class,
        StandaloneDataConfig.class,
        JndiDataConfig.class,
        DefaultDataConfig.class})
@ActiveProfiles("dev")
class TransferServiceTest {
    @Autowired
    TransferService transferService;
    @Test
    void testTransferService() {
        // тестируем transferService
    }
}
Kotlin
@SpringJUnitConfig(
        TransferServiceConfig::class,
        StandaloneDataConfig::class,
        JndiDataConfig::class,
        DefaultDataConfig::class)
@ActiveProfiles("dev")
class TransferServiceTest {
    @Autowired
    lateinit var transferService: TransferService
    @Test
    fun testTransferService() {
        // тестируем transferService
    }
}

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

  • TransferServiceConfig: Получает dataSource через внедрение зависимостей с помощью аннотации @Autowired.

  • StandaloneDataConfig: Определяет dataSource для встроенной базы данных, подходящий для тестов разработчика.

  • JndiDataConfig: Определяет dataSource, который извлекается из JNDI в производственной среде.

  • DefaultDataConfig: Определяет dataSource для встроенной базы данных по умолчанию, если профиль не активен.

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

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

Начиная со Spring Framework 5.3, тестовая конфигурация может также наследоваться от объемлющих классов. См. подробности в разделе "Конфигурация тестового класса с аннотацией @Nested".
Java
@SpringJUnitConfig({
        TransferServiceConfig.class,
        StandaloneDataConfig.class,
        JndiDataConfig.class,
        DefaultDataConfig.class})
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
Kotlin
@SpringJUnitConfig(
        TransferServiceConfig::class,
        StandaloneDataConfig::class,
        JndiDataConfig::class,
        DefaultDataConfig::class)
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
Java
// профиль "dev" наследуется от суперкласса
class TransferServiceTest extends AbstractIntegrationTest {
    @Autowired
    TransferService transferService;
    @Test
    void testTransferService() {
        // тестируем transferService
    }
}
Kotlin
// профиль "dev" наследуется от суперкласса
class TransferServiceTest : AbstractIntegrationTest() {
    @Autowired
    lateinit var transferService: TransferService
    @Test
    fun testTransferService() {
        // тестируем transferService
    }
}

Аннотация @ActiveProfiles также поддерживает атрибут inheritProfiles, который можно использовать для дезактивации наследования активных профилей, как показано в следующем примере:

Java
// профиль "dev" переопределяется на "production"
@ActiveProfiles(profiles = "production", inheritProfiles = false)
class ProductionTransferServiceTest extends AbstractIntegrationTest {
    // тело теста
}
Kotlin
// профиль "dev" переопределяется на "production"
@ActiveProfiles("production", inheritProfiles = false)
class ProductionTransferServiceTest : AbstractIntegrationTest() {
    // тело теста
}

Более того, иногда бывает необходимо разрешить активные профили для тестов программно, а не декларативно – например, исходя из:

  • Текущей операционной системы.

  • Факта выполнения тестов на сервере сборки для обеспечения непрерывной интеграции

  • Наличия определенных переменных окружения.

  • Наличия пользовательских аннотаций на уровне классов.

  • Другой сквозной функциональности.

Чтобы программно разрешать профили определения активных бинов, можно реализовать пользовательский ActiveProfilesResolver и зарегистрировать его с помощью атрибута resolver в аннотации @ActiveProfiles. Для получения дополнительной информации см. соответствующий javadoc. В следующем примере продемонстрировано, как реализовать и зарегистрировать пользовательский OperatingSystemActiveProfilesResolver:

Java
// профиль "dev" переопределяется программно с помощью пользовательского распознавателя
@ActiveProfiles(
        resolver = OperatingSystemActiveProfilesResolver.class,
        inheritProfiles = false)
class TransferServiceTest extends AbstractIntegrationTest {
    // тело теста
}
Kotlin
// профиль "dev" переопределяется программно с помощью пользовательского распознавателя
@ActiveProfiles(
        resolver = OperatingSystemActiveProfilesResolver::class,
        inheritProfiles = false)
class TransferServiceTest : AbstractIntegrationTest() {
    // тело теста
}
Java
public class OperatingSystemActiveProfilesResolver implements ActiveProfilesResolver {
    @Override
    public String[] resolve(Class<?> testClass) {
        String profile = ...;
        // определяем значение профиля, исходя из операционной системы
        return new String[] {profile};
    }
}
Kotlin
class OperatingSystemActiveProfilesResolver : ActiveProfilesResolver {
    override fun resolve(testClass: Class<*>): Array<String> {
        val profile: String = ...
        // определяем значение профиля, исходя из операционной системы
        return arrayOf(profile)
    }
}
Конфигурация контекста с использованием источников тестовых свойств

Spring Framework предлагает первоклассные средства поддержки понятия окружения с иерархией источников свойств, поэтому сконфигурировать интеграционные тесты можно с использованием источников тестовых свойств. В отличие от аннотации @PropertySource, используемой в классах @Configuration, аннотацию @TestPropertySource можно объявить в тестовом классе, чтобы объявлять местоположения ресурсов для файлов тестовых свойств или встраиваемых свойств. Эти источники тестовых свойств добавляются в набор PropertySources в Environment для ApplicationContext, загруженного для аннотированного интеграционного теста.

Можно использовать аннотацию @TestPropertySource с любой реализацией SPI-интерфейса SmartContextLoader, но аннотация @TestPropertySource не поддерживается при использовании реализации более старого SPI-интерфейса ContextLoader.

Реализации SmartContextLoader получают доступ к значениям объединенного источника тестовых свойств через методы getPropertySourceLocations() и getPropertySourceProperties() в MergedContextConfiguration.

Объявление источников тестовых свойств

Вы можете настроить файлы тестовых свойств, используя атрибут locations или value в аннотации @TestPropertySource.

Поддерживаются как традиционные, так и основанные на XML форматы файлов свойств – например, "classpath:/com/example/test.properties" или "file:///path/to/file.xml".

Каждый путь интерпретируется как Resource из Spring. Простой путь (например, "test.properties") считается ресурсом classpath, который относиться к пакету, в котором определен тестовый класс. Путь, начинающийся со косой черты, рассматривается как абсолютный ресурс classpath (например: "/org/example/test.xml"). Путь, ссылающийся на URL-адрес (например, путь с префиксом classpath:, file: или http:), загружается с использованием указанного протокола ресурсов. Подстановочные знаки местоположения ресурсов (например, */.properties) не допускаются: Каждое местоположение должно представлять собой ровно один ресурс .properties или .xml.

В следующем примере используется файл тестовых свойств:

Java
@ContextConfiguration
@TestPropertySource("/test.properties")
class MyIntegrationTests {
    // тело класса...
}
  1. Задаем файл свойств с абсолютным путем.
Kotlin
@ContextConfiguration
@TestPropertySource("/test.properties")
class MyIntegrationTests {
    // тело класса...
}
  1. Задаем файл свойств с абсолютным путем.

Можно сконфигурировать встраиваемые свойства в виде пар ключ-значение, используя атрибут properties в аннотации @TestPropertySource, как показано в следующем примере. Все пары ключ-значение добавляются в объемлющую Environment как один тестовый PropertySource с наивысшим уровнем старшинства.

Поддерживаемый синтаксис для пар ключ-значение такой же, как и синтаксис, определенный для записей в файле свойств Java:

  • key=value

  • key:value

  • key value

В следующем примере заданы два встраиваемых свойства:

Java
@ContextConfiguration
@TestPropertySource(properties = {"timezone = GMT", "port: 4242"})
class MyIntegrationTests {
    // тело класса...
}
  1. Задание двух свойств с помощью двух вариантов синтаксиса "ключ-значение".
Kotlin
@ContextConfiguration
@TestPropertySource(properties = ["timezone = GMT", "port: 4242"])
class MyIntegrationTests {
    // тело класса...
}
  1. Задание двух свойств с помощью двух вариантов синтаксиса "ключ-значение".

Начиная с версии Spring Framework 5.2, аннотацию @TestPropertySource можно использовать как повторяющуюся аннотацию. Это означает, можно иметь несколько объявлений аннотации @TestPropertySource в одном тестовом классе, при этом locations и properties из последующих аннотаций @TestPropertySource будут переопределять свойства из предыдущих аннотаций @TestPropertySource.

Кроме того, можно объявить несколько составных аннотаций для тестового класса, каждая из которых мета-анотирована с помощью @TestPropertySource, и все эти объявления аннотации @TestPropertySource внесут свой вклад в ваши источники тестовых свойств.

Аннотации @TestPropertySource, представленные напрямую, всегда имеют старшинство над мета-представленными аннотациями @TestPropertySource. Другими словами, locations и properties из непосредственно присутствующей аннотации @TestPropertySource будут переопределять locations и properties из аннотации @TestPropertySource, используемой в качестве мета-аннотации.

Обнаружение файла свойств по умолчанию

Если аннотация @TestPropertySource объявлена как пустая аннотация (то есть без явных значений для атрибутов locations или properties ), предпринимается попытка обнаружить файл свойств по умолчанию относительно класса, объявившего аннотацию. Например, если аннотированный тестовый класс - com.example.MyTest, то соответствующий файл свойств по умолчанию - classpath:com/example/MyTest.properties. Если значение по умолчанию не может быть определено, будет сгенерировано исключение IllegalStateException.

Старшинство

Тестовые свойства имеют более высокий уровень старшинства, чем свойства, определенные в среде операционной системы, системные свойства Java или источники свойств, добавленные приложением декларативно с помощью аннотации @PropertySource или программно. Таким образом, тестовые свойства можно использовать для выборочного переопределения свойств, загруженных из источников свойств системы и приложения. Кроме того, встраиваемые свойства имеют более высокий уровень старшинства, чем свойства, загруженные из местоположений ресурсов. Обратите внимание, однако, что свойства, зарегистрированные через аннотацию @DynamicPropertySource, имеют более высокий уровень старшинства, чем свойства, загруженные через аннотацию @TestPropertySource.

В следующем примере свойства timezone и port, а также любые свойства, определенные в файле "/test.properties", переопределяют любые одноименные свойства, определенные в источниках свойств системы и приложения. Более того, если в файле "/test.properties" определены записи для свойств timezone и port, то они переопределяются встроенными свойствами, объявленными с помощью атрибута properties. В следующем примере показано, как задать свойства как в файле, так и встроенно:

Java
@ContextConfiguration
@TestPropertySource(
    locations = "/test.properties",
    properties = {"timezone = GMT", "port: 4242"}
)
class MyIntegrationTests {
    // тело класса...
}
Kotlin
@ContextConfiguration
@TestPropertySource("/test.properties",
        properties = ["timezone = GMT", "port: 4242"]
)
class MyIntegrationTests {
    // тело класса...
}
Наследование и переопределение источников тестовых свойств

Аннотация @TestPropertySource поддерживает булевы атрибутыinheritLocations и inheritProperties, которые обозначают, следует ли наследовать местоположения ресурсов для файлов свойств и встраиваемых свойств, объявленных суперклассами. Значением по умолчанию для обоих флагов является true. Это означает, что тестовый класс наследует местоположения и встраиваемые свойства, объявленные любыми суперклассами. В частности, местоположения и встраиваемые свойства для тестового класса добавляются к местоположениям и встраиваемым свойствам, объявленным суперклассами. Таким образом, подклассы могут расширять местоположения и встраиваемые свойства. Обратите внимание, что свойства, которые появляются позже, "затеняют" (то есть переопределяют) одноименные свойства, которые появляются раньше. Кроме того, вышеупомянутые правила старшинства применяются и для наследуемых источников тестовых свойств.

Если атрибут inheritLocations или inheritPropertie.s в аннотации @TestPropertySource установлен в false, то местоположения или встраиваемые свойства для тестового класса, соответственно, "затеняют" и эффективно заменяют конфигурацию, определенную суперклассами.

Начиная со Spring Framework 5.3, тестовая конфигурация может также наследоваться от объемлющих классов. См. подробности в разделе "Конфигурация тестового класса с аннотацией @Nested".

В следующем примере ApplicationContext для BaseTest загружается только с использованием файла base.properties в качестве источника тестовых свойств. В то же время ApplicationContext для ExtendedTest загружается с использованием файлов base.properties и extended.properties в качестве источников тестовых свойств. В следующем примере показано, как определить свойства как в подклассе, так и в его суперклассе с помощью файлов properties:

Java
@TestPropertySource("base.properties")
@ContextConfiguration
class BaseTest {
    // ...
}
@TestPropertySource("extended.properties")
@ContextConfiguration
class ExtendedTest extends BaseTest {
    // ...
}
Kotlin
@TestPropertySource("base.properties")
@ContextConfiguration
open class BaseTest {
    // ...
}
@TestPropertySource("extended.properties")
@ContextConfiguration
class ExtendedTest : BaseTest() {
    // ...
}

В следующем примере ApplicationContext для BaseTest загружается только с помощью только встраиваемого свойства key1. В то же время ApplicationContext для ExtendedTest загружается с помощью встраиваемых свойств key1 и key2. В следующем примере показано, как определить свойства как в подклассе, так и в его суперклассе с помощью встраиваемых свойств:

Java
@TestPropertySource(properties = "key1 = value1")
@ContextConfiguration
class BaseTest {
    // ...
}
@TestPropertySource(properties = "key2 = value2")
@ContextConfiguration
class ExtendedTest extends BaseTest {
    // ...
}
Kotlin
@TestPropertySource(properties = ["key1 = value1"])
@ContextConfiguration
open class BaseTest {
    // ...
}
@TestPropertySource(properties = ["key2 = value2"])
@ContextConfiguration
class ExtendedTest : BaseTest() {
    // ...
}
Конфигурация контекста с использованием источников динамических свойств

Начиная со Spring Framework 5.2.5, фреймворк TestContext обеспечивает поддержку динамических свойств при помощи аннотации @DynamicPropertySource. Эту аннотацию можно использовать в интеграционных тестах, в которых необходимо добавить свойства с динамическими значениями в набор PropertySources в Environment для ApplicationContext, загруженного для интеграционного теста.

Аннотация @DynamicPropertySource и поддерживающая ее инфраструктура изначально были разработаны для того, чтобы свойства из тестов, основанных на Testcontainers, можно было легко открыть для интеграционных тестов Spring. Однако данную функцию также можно использовать с любой формой внешнего ресурса, жизненный цикл которого поддерживается вне ApplicationContext теста.

В отличие от аннотации @TestPropertySource, которая применяется на уровне класса, @DynamicPropertySource нужно применять к статическому методу, принимающему единственный аргумент DynamicPropertyRegistry, который используется для добавления пар "имя-значение" в Environment. Значения являются динамическими и указываются через Supplier, который вызывается только при разрешении свойства. Обычно для указания значений используются ссылки на методы, как видно из следующего примера, в котором проект Testcontainers используется для управления контейнером Redis вне ApplicationContext из Spring. IP-адрес и порт управляемого контейнера Redis доступны компонентам в ApplicationContext теста через свойства redis.host и redis.port. Доступ к этим свойствам можно получить через абстракцию Environment из Spring или внедрить непосредственно в управляемые Spring компоненты – например, через аннотации @Value("${redis.host}".) и @Value("${redis.port}"), соответственно.

Если аннотации @DynamicPropertySource используется в основном классе и обнаруживается, что тесты в подклассах не были пройдены из-за того, что динамические свойства меняются между подклассами, то может понадобиться аннотировать основной класс с помощью @DirtiesContext, чтобы каждый подкласс гарантированно получил свой собственный ApplicationContext с корректными динамическими свойствами.

Java
@SpringJUnitConfig(/* ... */)
@Testcontainers
class ExampleIntegrationTests {
    @Container
    static RedisContainer redis = new RedisContainer();
    @DynamicPropertySource
    static void redisProperties(DynamicPropertyRegistry registry) {
        registry.add("redis.host", redis::getContainerIpAddress);
        registry.add("redis.port", redis::getMappedPort);
    }
    // тесты...
}
Kotlin
@SpringJUnitConfig(/* ... */)
@Testcontainers
class ExampleIntegrationTests {
    companion object {
        @Container
        @JvmStatic
        val redis: RedisContainer = RedisContainer()
        @DynamicPropertySource
        @JvmStatic
        fun redisProperties(registry: DynamicPropertyRegistry) {
            registry.add("redis.host", redis::getContainerIpAddress)
            registry.add("redis.port", redis::getMappedPort)
        }
    }
    // тесты...
}
Старшинство

Динамические свойства имеют более высокий уровень старшинства, чем свойства, загруженные из аннотации @TestPropertySource, окружения операционной системы, системных свойств Java или источников свойств, добавленных приложением декларативно с помощью аннотации @PropertySource или программно. Таким образом, динамические свойства можно использовать для выборочного переопределения свойств, загруженных через аннотацию @TestPropertySource, системные источники свойств и источники свойств приложений.

Загрузка WebApplicationContext

Чтобы дать фреймворку TestContext команду загружать WebApplicationContext вместо стандартного ApplicationContext, можно аннотировать соответствующий тестовый класс с помощью @WebAppConfiguration.

Наличие аннотации @WebAppConfiguration в вашем тестовом классе указывает фреймворку TestContext (TCF), что для ваших интеграционных тестов должен быть загружен WebApplicationContext (WAC). В фоновом режиме TCF обеспечивает, что MockServletContext будет создан и передан в WAC вашего теста. По умолчанию путь к базовым ресурсам для вашего MockServletContext установлен в src/main/webapp. Он интерпретируется как путь относительно корня вашей JVM (обычно это путь к вашему проекту). Если вы знакомы со структурой каталогов веб-приложения в проекте Maven, то знаете, что src/main/webapp является местоположение по умолчанию для корня вашего WAR. Если нужно переопределить это значение по умолчанию, то можно указать альтернативный путь в аннотации @WebAppConfiguration (например, @WebAppConfiguration("src/test/webapp")). Если нужно сослаться на путь к основному ресурсу из пути классов, а не из файловой системы, то можно использовать префикс classpath: из Spring.

Обратите внимание, что поддержка тестирования для реализаций WebApplicationContext в Spring находится на одном уровне с поддержкой стандартных реализаций ApplicationContext. При тестировании с помощью WebApplicationContext можно свободно объявлять конфигурационные XML-файлы, скрипты Groovy или классы, помеченные аннотацией@Configuration, с помощью аннотации @ContextConfiguration. Вы также можете свободно использовать любые другие тестовые аннотации, такие как @ActiveProfiles, @TestExecutionListeners, @Sql, @Rollback и другие.

Остальные примеры в этом разделе демонстрируют некоторые из различных вариантов конфигурации для загрузки WebApplicationContext. В следующем примере показана поддержка фреймворком TestContext приоритета соглашения над конфигурацией:

Java
@ExtendWith(SpringExtension.class)
// по умолчанию "file:src/main/webapp"
@WebAppConfiguration
// обнаруживает "WacTests-context.xml" в том же пакете
// или статические вложенные классы с аннотацией @Configuration
@ContextConfiguration
class WacTests {
    //...
}
Kotlin
@ExtendWith(SpringExtension::class)
// по умолчанию "file:src/main/webapp"
@WebAppConfiguration
// обнаруживает "WacTests-context.xml" в том же пакете
// или статические вложенные классы с аннотацией @Configuration
@ContextConfiguration
class WacTests {
    //...
}

Если вы помечаете тестовый класс с помощью аннотации @WebAppConfiguration без указания пути к базе ресурсов, то путь к ресурсам фактически принимает значение по умолчанию file:src/main/webapp. Аналогично, если вы объявите аннотацию @ContextConfiguration без указания locations ресурсов, компонентных classes или initializers контекста, то Spring пытается обнаружить наличие вашей конфигурации, используя соглашения (то есть WacTests-context.xml в том же пакете, что и класс WacTests или статические вложенные классы, аннотированные @Configuration ).

В следующем примере показано, как явно объявлять путь к базе ресурсов с помощью аннотации @WebAppConfiguration и местоположение XML-ресурсов с помощью аннотации @ContextConfiguration:

Java
@ExtendWith(SpringExtension.class)
// ресурс файловой системы
@WebAppConfiguration("webapp")
// ресурс classpath
@ContextConfiguration("/spring/test-servlet-config.xml")
class WacTests {
    //...
}
Kotlin
@ExtendWith(SpringExtension::class)
// ресурс файловой системы
@WebAppConfiguration("webapp")
// ресурс classpath
@ContextConfiguration("/spring/test-servlet-config.xml")
class WacTests {
    //...
}

Здесь важно отметить разную семантику для путей при этих двух аннотациях. По умолчанию пути к ресурсам, аннотированные @WebAppConfiguration, основаны на файловой системе, в то время как местоположения ресурсов, аннотированных @ContextConfiguration, основаны на classpath.

В следующем примере показано, что мы можем переопределить семантику ресурсов по умолчанию для обеих аннотаций, указав префикс ресурсов Spring:

Java
@ExtendWith(SpringExtension.class)
// ресурс classpath
@WebAppConfiguration("classpath:test-web-resources")
// ресурс файловой системы
@ContextConfiguration("file:src/main/webapp/WEB-INF/servlet-config.xml")
class WacTests {
    //...
}
Kotlin
@ExtendWith(SpringExtension::class)
// ресурс classpath
@WebAppConfiguration("classpath:test-web-resources")
// ресурс файловой системы
@ContextConfiguration("file:src/main/webapp/WEB-INF/servlet-config.xml")
class WacTests {
    //...
}

Сравните комментарии в этом примере с предыдущим.

Работа с веб-имитациями

Чтобы обеспечить всестороннюю поддержку веб-тестирования, фреймворк TestContext имеет ServletTestExecutionListener, который активирован по умолчанию. При тестировании на WebApplicationContext этот слушатель TestExecutionListener устанавливает локальное состояние потока по умолчанию с помощью RequestContextHolder из Spring Web перед каждым тестовым методом и создает MockHttpServletRequest, MockHttpServletResponse и ServletWebRequest на основе пути к основному ресурсу, сконфигурированного с помощью @WebAppConfiguration. Слушатель ServletTestExecutionListener также обеспечивает, чтоб MockHttpServletResponse и ServletWebRequest могли быть внедрены в тестовый экземпляр, а после завершения теста он очищает локальное состояние потока.

После загрузки WebApplicationContext для вашего теста можно обнаружить, что необходимо взаимодействовать с веб-имитациями – например, для настройки тестового стенда или для выполнения утверждений после вызова вашего веб-компонента. В следующем примере показано, какие объекты-имитации можно автоматически обнаружить и связать с вашим экземпляром теста. Обратите внимание, что WebApplicationContext и MockServletContext кэшируются для всего тестового комплекта, в то время как другие объекты-имитации управляются каждым тестовым методом с помощью ServletTestExecutionListener.

Java
@SpringJUnitWebConfig
class WacTests {
    @Autowired
    WebApplicationContext wac; // кэшируется
    @Autowired
    MockServletContext servletContext; // кэшируется
    @Autowired
    MockHttpSession session;
    @Autowired
    MockHttpServletRequest request;
    @Autowired
    MockHttpServletResponse response;
    @Autowired
    ServletWebRequest webRequest;
    //...
}
Kotlin
@SpringJUnitWebConfig
class WacTests {
    @Autowired
    lateinit var wac: WebApplicationContext // кэшируется
    @Autowired
    lateinit var servletContext: MockServletContext // кэшируется
    @Autowired
    lateinit var session: MockHttpSession
    @Autowired
    lateinit var request: MockHttpServletRequest
    @Autowired
    lateinit var response: MockHttpServletResponse
    @Autowired
    lateinit var webRequest: ServletWebRequest
    //...
}
Кэширование контекста

Как только фреймворк TestContext загрузит ApplicationContext (или WebApplicationContext) для теста, этот контекст кэшируется и повторно используется для всех последующих тестов, которые объявляют ту же уникальную контекстную конфигурацию в рамках одного тестового комплекта. Чтобы понять, как работает кэширование, важно понимать, что подразумевается под "уникальным" и "тестовым комплектом".

ApplicationContext можно однозначно идентифицировать по комбинации конфигурационных параметров, которая используется для его загрузки. Следовательно, уникальная комбинация конфигурационных параметров используется для генерации ключа, под которым кэшируется контекст. Фреймворк TestContext использует следующие конфигурационные параметры для построения ключа контекстного кэша:

  • locations@ContextConfiguration)

  • classes@ContextConfiguration)

  • contextInitializerClasses@ContextConfiguration)

  • contextCustomizers (из ContextCustomizerFactory) – сюда входят методы, помеченные аннотацией @DynamicPropertySource, а также различные функции из средств поддержки тестирования Spring Boot, такие как аннотации @MockBean и @SpyBean.

  • contextLoader@ContextConfiguration)

  • parent@ContextHierarchy)

  • activeProfiles@ActiveProfiles)

  • propertySourceLocations@TestPropertySource)

  • propertySourceProperties@TestPropertySource)

  • resourceBasePath@WebAppConfiguration)

Например, если TestClassA задает {"app-config.xml", "test-config.xml".} для атрибута locations (или value), аннотированного @ContextConfiguration, фреймворк TestContext загружает соответствующий ApplicationContext и сохраняет его в статическом кэше контекста под ключом, который основан исключительно на этих местоположениях. Так, если TestClassB также определяет {"app-config.xml", "test-config.xml"} для своих местоположений (явно или неявно через наследование), но не определяет аннотацию @WebAppConfiguration, другой загрузчик ContextLoader, другие активные профили, другие инициализаторы контекста, другие источники тестовых свойств или другой родительский контекст, то один и тот же ApplicationContext используется совместно обоими тестовыми классами. Это означает, что ресурсы на загрузку контекста приложения затрачиваются только один раз (для каждого тестового комплекта), а последующее выполнение тестов происходит гораздо быстрее.

Тестовые комплекты и разветвленные процессы

Фреймворк TestContext из Spring хранит контексты приложений в статическом кэше. Это означает, что контекст буквально хранится в статической переменной. Другими словами, если тесты выполняются в отдельных процессах, статический кэш очищается между каждым выполнением теста, что позволяет эффективно дезактивировать механизм кэширования.

Чтобы воспользоваться преимуществами механизма кэширования, все тесты должны выполняться в рамках одного процесса или тестового комплекта. Этого можно достичь, выполняя все тесты как группу в IDE. Точно так же при выполнении тестов с помощью такого фреймворка сборки, как Ant, Maven или Gradle, важно убедиться, что фреймворк сборки не разветвляется между тестами. Например, если forkMode для плагина Maven Surefire установлен в always или pertest, фреймворк TestContext не сможет кэшировать контексты приложений между тестовыми классами, в результате чего процесс сборки будет выполняться значительно медленнее.

Размер контекстного кэша ограничен, максимальный размер по умолчанию составляет 32. При достижении максимального размера используется политика вытеснения по давности использования (LRU) для вытеснения и закрытия устаревших контекстов. Можно настроить максимальный размер из командной строки или скрипта сборки, установив системное свойство JVM под названием spring.test.context.cache.maxSize. Как вариант, можно установить это же свойство через механизм SpringProperties.

Поскольку большое количество контекстов приложения, загруженных в конкурентном тестовом комплекте, может привести к тому, что комплект будет выполняться слишком долго, зачастую полезно знать, сколько именно контекстов было загружено и кэшировано. Чтобы просмотреть статистику для базового контекстного кэша, можно установить уровень ведения журнала для категории журналирования org.springframework.test.context.cache в значение DEBUG.

В маловероятном случае, если тест повредит контекст приложения и потребует перезагрузки (например, путем изменения определения бина или состояния объекта приложения), можно аннотировать свой тестовый класс или тестовый метод с помощью @DirtiesContext (см. описание аннотации @DirtiesContext в разделе "Аннотации для тестирования в Spring"). Таким образом Spring задается команда удалить контекст из кэша и перестроить контекст приложения перед запуском следующего теста, которому требуется тот же контекст приложения. Обратите внимание, что поддержка аннотации @DirtiesContext обеспечивается слушателями DirtiesContextBeforeModesTestExecutionListener и DirtiesContextTestExecutionListener, которые активированы по умолчанию.

Жизненный цикл ApplicationContext и вывод сообщений на консоль

Если необходимо отладить тест, выполненный с помощью Spring TestContext Framework, может быть полезно проанализировать консольный вывод (то есть вывод в потоки SYSOUT и SYSERR ). Некоторые встроенные инструментальные средства сборки и IDE могут связывать консольный вывод с определенным тестом; однако легко связать некоторые консольные выводы с определенным тестом не выйдет.

Что касается вывода сообщений на консоль, запускаемого самим Spring Framework или компонентами, зарегистрированными в ApplicationContext, важно понимать жизненный цикл ApplicationContext, который был загружен Spring TestContext Framework в тестовом комплекте.

ApplicationContext для теста обычно загружается при подготовке экземпляра тестового класса – например, для выполнения внедрения зависимостей в поля тестового экземпляра, помеченные аннотацией @Autowired. Это означает, что любой вывод сообщений на консоль, запущенный во время инициализации ApplicationContext, обычно нельзя связать с отдельным тестовым методом. Однако, если контекст закрывается непосредственно перед выполнением тестового метода в соответствии с семантикой аннотации @DirtiesContext, новый экземпляр контекста будет загружен непосредственно перед выполнением тестового метода. В последнем сценарии IDE или встроенное инструментальное средство сборки может потенциально связывать вывод сообщений на консоль с отдельным тестовым методом.

ApplicationContext для теста может быть закрыт по одному из указанных далее сценариев.

  • Контекст закрывается в соответствии с семантикой аннотации @DirtiesContext.

  • Контекст закрывается, потому что был автоматически удален из кэша в соответствии с политикой вытеснения LRU.

  • Контекст закрывается через перехватчик завершения JVM, когда работа JVM для тестового комплекта прекращается.

Если контекст закрывается в соответствии с семантикой аннотации @DirtiesContext после определенного тестового метода, IDE или встроенное инструментальное средство сборки потенциально может связать вывод сообщений на консоль с отдельным тестовым методом. Если контекст закрывается в соответствии с семантикой @DirtiesContext после выполнения тестового класса, любой вывод сообщений на консоль, срабатывающий во время закрытия ApplicationContext, нельзя связать с отдельным тестовым методом. Кроме того, любой вывод сообщений на консоль, срабатывающий во время фазы завершения через перехватчик завершения из JVM, нельзя связать с отдельным тестовым методом.

Если ApplicationContext из Spring закрывается через перехватчик выключения из JVM, обратные вызовы, выполняемые во время фазы завершения, выполняются в потоке с именем SpringContextShutdownHook. Таким образом, если нужно дезактивировать вывод сообщений на консоль, срабатывающий при закрытии ApplicationContext через перехватчик завершения работы из JVM, можно зарегистрировать пользовательский фильтр в платформе журналирования, которая позволит игнорировать любое журналирование, инициированное этим потоком.

Иерархии контекстов

При написании интеграционных тестов, которые используют нагруженный ApplicationContext из Spring, часто бывает достаточно провести тестирование относительно одного контекста. Однако бывают случаи, когда полезно или даже необходимо проводить тестирование относительно иерархии экземпляров ApplicationContext. Например, если вы разрабатываете веб-приложение в Spring MVC, то у вас обычно есть корневой WebApplicationContext, загружаемый ContextLoaderListener из Spring, а также дочерний WebApplicationContext, загружаемый DispatcherServlet из Spring. Это приводит к созданию иерархии контекстов "родитель-ребенок", где общие компоненты и конфигурация инфраструктуры объявляются в корневом контексте и потребляются в дочернем контексте компонентами, специфичными для веб. Другой пример использования встречается в приложениях Spring Batch, где зачастую существует родительский контекст, который обеспечивает конфигурацию общей пакетной инфраструктуры, и дочерний контекст для конфигурации конкретного пакетного задания.

Можно писать интеграционные тесты, использующие иерархии контекстов, объявляя конфигурацию контекста с помощью аннотации @ContextHierarchy либо для отдельного тестового класса, либо внутри иерархии тестовых классов. Если иерархия контекстов объявлена для нескольких классов в иерархии тестовых классов, также можно объединить или переопределить конфигурацию контекста для определенного именованного уровня в иерархии контекстов. При объединении конфигурации для данного уровня иерархии тип конфигурационных ресурсов (то есть конфигурационные XML-файлы или компонентные классы) должен быть согласованным. В противном случае, вполне допустимо иметь разные уровни в иерархии контекстов, сконфигурированные с использованием разных типов ресурсов.

В остальных основывающихся на JUnit Jupiter примерах из этого раздела наглядно видно общие сценарии конфигурации для интеграционных тестов, которые требуют использования иерархий контекстов.

Единственный тестовый класс с иерархией контекстов

ControllerIntegrationTests представляет типичный сценарий интеграционного тестирования для веб-приложения в Spring MVC, объявляя иерархию контекстов, состоящую из двух уровней; один для корневого WebApplicationContext (загруженного с помощью класса TestAppConfig @Configuration), а другой – для сервлета-диспетчера WebApplicationContext (загруженного с помощью класса WebConfig @Configuration). WebApplicationContext, который автоматически обнаруживается и связывается с экземпляром теста, является контекстом для дочернего контекста (то есть самым контекстом самого низкого уровня в иерархии). В следующем листинге показан этот сценарий конфигурации:

Java
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextHierarchy({
    @ContextConfiguration(classes = TestAppConfig.class),
    @ContextConfiguration(classes = WebConfig.class)
})
class ControllerIntegrationTests {
    @Autowired
    WebApplicationContext wac;
    // ...
}
Kotlin
@ExtendWith(SpringExtension::class)
@WebAppConfiguration
@ContextHierarchy(
    ContextConfiguration(classes = [TestAppConfig::class]),
    ContextConfiguration(classes = [WebConfig::class]))
class ControllerIntegrationTests {
    @Autowired
    lateinit var wac: WebApplicationContext
    // ...
}
Иерархия классов с неявным родительским контекстом

Тестовые классы в этом примере определяют иерархию контекстов внутри иерархии тестовых классов. AbstractWebTests объявляет конфигурацию для корневого WebApplicationContext в веб-приложении на базе Spring. Обратите внимание, однако, что AbstractWebTests не объявляет аннотацию @ContextHierarchy. Следовательно, подклассы AbstractWebTests могут опционально участвовать в иерархии контекстов или следовать стандартной для аннотации @ContextConfiguration семантике. SoapWebServiceTests и RestWebServiceTests расширяют AbstractWebTests и определяют иерархию контекстов с помощью аннотации @ContextHierarchy. В результате загружаются три контекста приложения (по одному для каждого объявления аннотации @ContextConfiguration), а контекст приложения, загружаемый на основе конфигурации в AbstractWebTests, устанавливается в качестве родительского контекста для каждого из контекстов, загруженных для конкретных подклассов. В следующем листинге показан этот сценарий конфигурации:

Java
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public abstract class AbstractWebTests {}
@ContextHierarchy(@ContextConfiguration("/spring/soap-ws-config.xml"))
public class SoapWebServiceTests extends AbstractWebTests {}
@ContextHierarchy(@ContextConfiguration("/spring/rest-ws-config.xml"))
public class RestWebServiceTests extends AbstractWebTests {}
Kotlin
@ExtendWith(SpringExtension::class)
@WebAppConfiguration
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
abstract class AbstractWebTests
@ContextHierarchy(ContextConfiguration("/spring/soap-ws-config.xml"))
class SoapWebServiceTests : AbstractWebTests()
@ContextHierarchy(ContextConfiguration("/spring/rest-ws-config.xml"))
class RestWebServiceTests : AbstractWebTests()
Иерархия классов с объединенной конфигурацией иерархии контекстов

Классы в этом примере показывают использование именованных уровней иерархии для объединения конфигурации для определенных уровней в иерархии контекстов. BaseTests определяет два уровня в иерархии, а именно parent и child. ExtendedTests расширяет BaseTests и дает Spring TestContext Framework команду объединять конфигурацию контекста для child уровня иерархии, обеспечивая, чтоб имена, объявленные в атрибуте name в аннотации @ContextConfiguration, являлисьchild. В результате загружаются три контекста приложения: один для /app-config.xml, один для /user-config.xml, и один для {"/user-config.xml", "/order-config.xml"}. Как и в предыдущем примере, контекст приложения, загруженный из /app-config.xml, устанавливается в качестве родительского контекста для контекстов, загруженных из /user-config.xml и {"/user-config.xml", "/order-config.xml"}. В следующем листинге показан этот сценарий конфигурации:

Java
@ExtendWith(SpringExtension.class)
@ContextHierarchy({
    @ContextConfiguration(name = "parent", locations = "/app-config.xml"),
    @ContextConfiguration(name = "child", locations = "/user-config.xml")
})
class BaseTests {}
@ContextHierarchy(
    @ContextConfiguration(name = "child", locations = "/order-config.xml")
)
class ExtendedTests extends BaseTests {}
Kotlin
@ExtendWith(SpringExtension::class)
@ContextHierarchy(
    ContextConfiguration(name = "parent", locations = ["/app-config.xml"]),
    ContextConfiguration(name = "child", locations = ["/user-config.xml"]))
open class BaseTests {}
@ContextHierarchy(
    ContextConfiguration(name = "child", locations = ["/order-config.xml"])
)
class ExtendedTests : BaseTests() {}
Иерархия классов с переопределенной конфигурацией иерархии контекстов

В отличие от предыдущего примера, этот пример демонстрирует, как переопределить конфигурацию для данного именованного уровня в иерархии контекстов, установив флаг inheritLocations в аннотации @ContextConfiguration в false. Следовательно, контекст приложения для ExtendedTests загружается только из /test-user-config.xml, а его родителем является контекст, загруженный из /app-config.xml. В следующем листинге показан этот сценарий конфигурации:

Java
@ExtendWith(SpringExtension.class)
@ContextHierarchy({
    @ContextConfiguration(name = "parent", locations = "/app-config.xml"),
    @ContextConfiguration(name = "child", locations = "/user-config.xml")
})
class BaseTests {}
@ContextHierarchy(
    @ContextConfiguration(
        name = "child",
        locations = "/test-user-config.xml",
        inheritLocations = false
))
class ExtendedTests extends BaseTests {}
Kotlin
@ExtendWith(SpringExtension::class)
@ContextHierarchy(
    ContextConfiguration(name = "parent", locations = ["/app-config.xml"]),
    ContextConfiguration(name = "child", locations = ["/user-config.xml"]))
open class BaseTests {}
@ContextHierarchy(
        ContextConfiguration(
                name = "child",
                locations = ["/test-user-config.xml"],
                inheritLocations = false
        ))
class ExtendedTests : BaseTests() {}
Если вы используете аннотацию @DirtiesContext в тесте, контекст которого сконфигурирован как часть иерархии контекстов, можно использовать флаг hierarchyMode для управления очисткой контексного кэша. За более подробной информацией обратитесь к описанию аннотации @DirtiesContext в разделе "Аннотации для тестирования в Spring" и javadoc по аннотации @DirtiesContext.