[Перевод] За каким чертом нам SpringExtension?
Новый перевод от команды Spring АйО расскажет вам, что такое SpringExtension
, как правильно пользоваться этим расширением и когда его надо (или не надо) регистрировать вручную.
В последнее время я видел много путаницы по поводу SpringExtension
. Когда не удается правильно получить конфигурацию контекста для теста, некоторые разработчики рандомно выбрасывают @ExtendWith(SpringExtension.class)
в свои тестовые классы, чтобы тесты (как они надеются) заработали. В этом блог-посте я хочу пролить немного света на SpringExtension
и его использование при тестировании приложений на Spring Boot. К концу этой статьи вы поймете, когда, почему и как использовать это расширение.
SpringExtension
обеспечивает гладкую интеграцию тестов, написанных на JUnit Jupiter, с фреймворком TestContext от Spring. Чаще всего вам не надо в явном виде регистрировать SpringExtesion
, поскольку все test slice аннотации Spring Boot делают это сами.
Для чего используется расширение JUnit Jupiter?
Первый вопрос, на который нам надо ответить, если мы хотим понять SpringExtension
— это вопрос о том, что такое расширение JUnit Jupiter и зачем оно нужно.
Модель расширения JUnit Jupiter — это единая концепция (по контрасту с такими API от JUnit 4 как Runner
и Rule
), предназначенная для расширения функциональности тестов и перехватывания циклов жизни наших тестов программным путем. Существует довольно много различных точек внутри жизненного цикла, куда может встраиваться расширение (например, BeforeAllCallback
); кроме того, доступны и другие функции в форме утилит (например, ParameterResolver
). Чтобы увидеть полный список всех доступных API расширения, рекомендую посмотреть на интерфейс Extension
и на все интерфейсы, которые его расширяют.
Использовать их можно довольно гибко. Мы можем выполнить рутинные задачи до или после теста, разрешить параметры и решить, выполнять тест или пропустить его.
Чаще всего мы реализуем перекрестные аспекты, которые имеют отношения к нескольким тестам, использующим расширение. Например, при написании тестов для веб, вместо оборачивания взаимодействия браузера в блок try
-catch
для каждого теста, чтобы сделать скриншоты неудачного завершения, мы можем написать для этого расширение JUnit Jupiter предлагает интерфейс TestExecutionExceptionHandler
для обработки исключений в наших тестовых методах в одном централизованном месте. (PS: Selenide сразу поставляется с расширением, делающим скриншоты при неудачном завершении).
Написание кастомизированного расширения — наука несложная. Мы разрабатываем кастомизированное расширение, которое инжектирует случайные UUID
в наши тестовые методы как часть мастер-класса Testing Spring Boot Applications Masterclass.
Каждый раз, когда мы хотим активировать расширение JUnit Jupiter для нашего теста, мы должны в явном виде зарегистрировать его при помощи аннотации @ExtendWith
над тестовым классом:
@ExtendWith(MyExtension.class)
class MyTest {
@Test
void test() {
}
}
Многие фреймворки и тестовые библиотеки поставляются с кастомизированным расширением JUnit Jupiter для удобной интеграции с окружением JUnit.
Примеры:
MockitoExtension
от Mockito для простой настройки наших mock-овТестовые контейнеры
TestcontainersExtension
(активируются при помощи@Testcontainers
) для удобного запуска и остановки Docker контейнеровSpringExtension
от Spring
Чтобы узнать больше о модели расширения и ее различных API, см. JUnit 5 User Guide.
В чем цель SpringExtension?
На следующем шаге давайте исследуем перекрестную функциональность, которую реализует SpringExtension
.
Лучший способ начать наше расследование — посмотреть на исходный код SpringExtension
, чтобы понять, какие именно API расширения он реализует:
public class SpringExtension implements BeforeAllCallback,
AfterAllCallback,
TestInstancePostProcessor,
BeforeEachCallback,
AfterEachCallback,
BeforeTestExecutionCallback,
AfterTestExecutionCallback,
ParameterResolver {
// ...
}
Их довольно много. Как можно понять из приведенного выше исходного кода, SpringExtension
плотно взаимодействует с жизненным циклом наших тестов.
Если не погружаться слишком глубоко в детали реализации, главные зоны ответственности этого расширения следующие:
Управлять жизненным циклом
TestContext
Spring (например, начать новый цикл получения кешированного контекста)Поддерживать инжекцию зависимостей для параметров (например, конструктор тестового класса или тестовый метод)
Задачи по чистке и другие «хозяйственные» рутинные задачи, выполняемые после теста
SpringExtension
работает как клей между JUnit Jupiter и Spring Test. Чаще всего SpringExtension
делегирует свои обязанности TestContextManager
, чтобы он выполнил самую тяжелую работу:
public class SpringExtension {
@Override
public void beforeAll(ExtensionContext context) throws Exception {
getTestContextManager(context).beforeTestClass();
}
}
TestContextManager
отвечает за один TestContext
и не зависит от тестового фреймворка. Более того, TestContextManager
также вызывает все зарегистрированные TestContextListeners
на основании события жизненного цикла (например, до тестового класса или после тестового метода).
Теперь посмотрим на еще один практический пример работы SpringExtension
: разрешение (инжектирование) параметров:
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class ApplicationIT {
@Test
void needsEnvironmentBeanToVerifySomething(
@Autowired Environment environment) { // resolved by the SpringExtension
assertNotNull(environment);
}
}
Приведенный выше метод инжектирует Environment
через параметр метода. В фоновом режиме следующий фрагмент кода из SpringExtension
берет на себя ответственность за разрешение этого параметра:
public class SpringExtension {
// ...
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
// determine whether or not this extension is responsible to resolve
// the parameter
}
@Nullable
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
// return the bean from the TestContext
}
}
Прежде чем SpringExtension
попытается разрешить данный параметр, supportsParameter
(часть интерфейса ParameterResolver
) определяет, является ли это обязанностью данного расширения. В нашем примере аннотация @Autowired
рядом с параметром является явным индикатором того, что SpringExtension
должен разрешить этот параметр с помощью TestContext
.
Однако, инжектирование полей работает через DependencyInjectionTestExecutionListener
. Это один из многих заданных по умолчанию TestExecutionListeners
, которые TestContextManager
вызывает перед запуском любого теста.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class ApplicationIT {
@Autowired
// injected by the DependencyInjectionTestExecutionListener
private CustomerService customerService;
@Test
void needsEnvironmentBeanToVerifySomething(
@Autowired Environment environment // resolved by the SpringExtension
) {
assertNotNull(environment);
}
}
Когда нам необходимо регистрировать SpringExtension?
Чаще всего нам не нужно регистрировать это расширение явным образом, потому что оно уже активировано для нас. Так обстоят дела всякий раз, когда мы используем тестовые аннотации из Spring Boot.
Все Spring Boot test slice аннотации, а также @SpringBootTest, регистрируют SpringExtension
из коробки. И это хорошо, потому что в противном случае тесты не смогли бы стартовать, потому что им для работы был бы нужен TestContext
. Поэтому данный подход избавляет нас от необходимости нажимать на клавиши несколько лишних раз, и кроме того мы не сталкиваемся со странными падениями тестов из-за того, что мы забыли зарегистрировать SpringExtension
.
Посмотрев на исходный код аннотации @WebMvcTest
, мы увидим вот это:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(WebMvcTestContextBootstrapper.class)
@ExtendWith({SpringExtension.class})
// ... further annotations
public @interface WebMvcTest {
}
Среди других мета-аннотаций и инструкций по поводу того, что необходимо автоматически сконфигурировать для тестов такого типа, мы видим, что этот код активирует для нас SpringExtension
.
То же самое верно для @SpringBootTest
:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
// ...
public @interface SpringBootTest {
// ...
}
Поэтому каждый раз, когда мы встречаем тестовый класс, где SpringExtension
зарегистрирован вручную, и используется одна из test slice аннотаций Spring Boot, мы можем безопасно удалить его:
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ExtendWith(SpringExtension.class) // not necassary and should be removed
class ApplicationIT {
// ...
}
В то время как JUnit Jupiter не будет жаловаться (то есть заваливать тест или поднимать много шума в логах), если одно и то же расширение сконфигурировано дважды, мы должны удалить его, чтобы избежать такого дублирования. Это лишний код, который может только привести к путанице для новых членов команды, если мы не добавляем SpringExtension
стабильно везде.
Когда нам необходимо регистрировать SpringExtension (часть вторая)?
Существует ограниченный набор use case-ов, где мы обязаны явно зарегистрировать SpringExtension
вручную. Один такой use case — это написание кастомизированной test slice аннотации.
Как часть мастер-класса Testing Spring Boot Applications Masterclass, мы загружаем компонент мессенджера в нашем приложении для тестовых целей. Этот практический пример демонстрирует, как загружать в наше приложение только релевантные части Amazon SQS Listener. Поскольку сейчас доступна test slice аннотация от Spring Boot, мы вручную регистрируем SpringExtension
, чтобы работать с TestContext
от Spring на протяжении всего теста:
@ExtendWith(SpringExtension.class)
@Import(BookSynchronizationListener.class)
@ImportAutoConfiguration(MessagingAutoConfiguration.class)
@Testcontainers(disabledWithoutDocker = true)
class BookSynchronizationListenerSliceTest {
// test a SQS listener in isolation
}
Заключение
SpringExtension
реализует несколько методов для вызова модели расширения JUnit Jupiter для гладкой интеграции между JUnit и Spring. Когда мы тестируем приложения на Spring Boot, нам как правило не нужно явно регистрировать расширение, поскольку все sliced context аннотации (например, @WebMvcTest
) делают это за нас.
Официальная документация тоже является прекрасным источником информации, если вы хотите глубже погрузиться в тему.
Исходный код приведенных выше примеров доступен на GitHub.
Наслаждайтесь работой со SpringExtension.

Регистрируйтесь на главную конференцию про Spring на русском языке от сообщества Spring АйО! В мероприятии примут участие не только наши эксперты, но и приглашенные лидеры индустрии.