Внедрение зависимостей (DI) в JUnit 5

Цель модульного теста — найти ошибки в приложении как можно раньше. Хотя существует несколько способов добиться этого, стоит выбирать наиболее эффективный.
В наборе тестов JUnit может быть несколько классов, которым нужны одинаковые данные, но повторно использовать тестовые данные бывает неудобно. Раньше обычным решением было написать вспомогательный метод и вызывать его из каждого тестового класса. JUnit 5 предлагает более удобный подход: внедрение зависимостей (DI).
Что такое внедрение зависимостей?
Внедрение зависимостей (Dependency Injection, DI) — это шаблон проектирования, при котором один объект предоставляет зависимости другому. Проще: если класс A зависит от объекта B для своей работы, DI позволяет предоставить B извне, а не создавать B внутри A.
Короткое определение терминов:
- DI: предоставление внешних зависимостей классу вместо их создания внутри класса.
- ParameterResolver: компонент JUnit, который умеет подставлять параметры в методы и конструкторы тестов.
DI в JUnit 5 — что изменилось
JUnit 5 разрешает параметризовать тестовые методы и конструкторы. Это важно, потому что предыдущие версии не допускали параметров в тестовых методах и конструкторах.
- Можно передавать сколько угодно параметров, но каждый параметр должен уметь разрешать ParameterResolver.
- Встроенные резолверы JUnit автоматически обрабатывают несколько типов (например, TestInfo). Для других типов нужно регистрировать расширения через @ExtendWith.
Пример: Injection с TestInfo
Ниже пример использует встроенный TestInfoParameterResolver — JUnit сам подставляет объект, реализующий интерфейс TestInfo, в конструктор и методы тестового класса.
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
class InfoTestInterfaceTest {
// Injecting a testInfo object into the InfoTestInterfaceTest constructor
InfoTestInterfaceTest(TestInfo testInfo) {
assertEquals("InfoTestInterfaceTest", testInfo.getDisplayName());
}
// Injecting a testInfo object into methods
@Test
void testMethodName(TestInfo testInfo) {
assertEquals("testMethodName(TestInfo)", testInfo.getDisplayName());
}
@Test
@DisplayName("method using the @DisplayName annotation")
void testMethodNameTwo(TestInfo testInfo) {
assertEquals("method using the @DisplayName annotation", testInfo.getDisplayName());
}
}
Код выше показывает: JUnit подставляет TestInfo в конструктор и методы. Метод getDisplayName() возвращает отображаемое имя текущего теста — по умолчанию оно выводится на основе класса и имени метода, но если вы используете аннотацию @DisplayName, будет возвращён текст из этой аннотации.

Внедрение в методы @BeforeEach, @AfterEach, @BeforeAll и @AfterAll
JUnit 5 также позволяет передавать зависимости в методы с аннотациями жизненного цикла: @BeforeAll, @BeforeEach, @AfterEach, @AfterAll. Достаточно добавить параметр в сигнатуру метода — и, если есть подходящий резолвер, JUnit подставит объект.
Важно: для @BeforeAll метод обычно должен быть статическим, если не используется @TestInstance(Lifecycle.PER_CLASS). При использовании PER_CLASS можно применять нестатические @BeforeAll и получать внедрённые параметры.
Почему использовать DI в тестах
- Уменьшает дублирование кода и конфигураций в разных классах тестов.
- Улучшает читаемость: зависимости представлены явно в подписи метода/конструктора.
- Упрощает настройку тестовой среды и переиспользование фикстур.
Когда DI не подходит
- Простые юнит-тесты, где зависимость тривиальна и нет повторного использования.
- Тесты для статических утилит, где DI не даёт преимуществ.
- Когда нужно строго контролировать время создания объекта (например, тестирование логики конструктора с побочными эффектами).
Альтернативные подходы
- Вспомогательные фабричные методы/утилиты: хороши для простых случаев.
- Аннотация @TestInstance(Lifecycle.PER_CLASS) для хранения состояния между методами без глобальных статиков.
- Параметризованные тесты (ParameterizedTest) для набора входных данных.
- Мокирование через Mockito/MockK для управления поведением зависимостей.
- Написание собственного ParameterResolver и регистрация через @ExtendWith для специфичных типов.
Быстрая методика внедрения DI в существующий набор тестов
- Определите повторяющиеся объекты/фикстуры, которые создаются в нескольких классах.
- Проверьте, есть ли встроенный резолвер (например, TestInfo). Если нет — решите, писать собственный или использовать расширение.
- Для нестатических жизненных методов рассмотрите @TestInstance(Lifecycle.PER_CLASS).
- Замените создание объектов на передачу их в конструктор/метод теста.
- Добавьте тесты на совместимость: сборка, запуск в IDE и CI.
Чек‑лист для ревью тестов
- Тесты явно принимают зависимости в сигнатуре метода/конструктора, а не создают их тайно.
- Нет дублирования кода для создания одной и той же фикстуры.
- Использование DI не скрывает побочных эффектов или общего состояния между тестами.
- Документированы нестандартные резолверы (через @ExtendWith).
Критерии приёмки
- Тесты компилируются и проходят в локальной среде и в CI.
- Снижение дублирования конфигурации/данных между тестовыми классами.
- Отсутствие флейковых тестов из‑за неявного состояния.
Короткий глоссарий
- DI: внедрение зависимостей, передача внешних объектов в класс/метод.
- ParameterResolver: интерфейс JUnit для подстановки параметров.
- TestInfo: интерфейс JUnit с информацией о текущем тесте (например, отображаемое имя).
- @ExtendWith: аннотация для подключения расширений/резолверов.
Decision tree — стоит ли использовать DI?
flowchart TD
A[Нужно ли переиспользовать фикстуру в нескольких тестах?] -->|Да| B[Есть встроенный резолвер?]
A -->|Нет| C[Оставьте фабрику/утилиту]
B -->|Да| D[Использовать DI в сигнатурах тестов]
B -->|Нет| E[Рассмотреть @ExtendWith или фабрики]
E --> F{Нужен ли специфичный резолвер?}
F -->|Да| G[Написать ParameterResolver и подключить]
F -->|Нет| H[Использовать утилиту/мок]Примеры ошибок и когда DI может подвести
- Скрытые зависимости: если тест принимает объект, но не ясно откуда он приходит, читаемость страдает.
- Состояние между тестами: неверная конфигурация резолвера может приводить к повторному использованию mutable-объектов.
- Неправильная регистрация расширения: резолверы не подставляются — тесты падают на этапе запуска.
Роль‑бейсд чек‑лист
- Автор теста:
- Явно указал зависимости в сигнатуре.
- Обеспечил чистоту состояния между тестами.
- Добавил примечание, если подключил нестандартный резолвер.
- Рецензент:
- Проверил отсутствие дублирования.
- Проверил, что внедрение не скрывает логику теста.
- Убедился, что тесты проходят в CI.
Важно: внедрение зависимостей улучшает структуру тестов, но требует дисциплины по управлению состоянием и ясной документации расширений.
Итог
Внедрение зависимостей в JUnit 5 — мощный инструмент для уменьшения дублирования, явного указания фикстур и повышения читаемости тестов. Используйте встроенные резолверы, подключайте свои через @ExtendWith при необходимости и следите за чистотой состояния между тестами.
Ключевые выводы:
- JUnit 5 поддерживает DI в конструкторах и методах тестов.
- TestInfo — удобный встроенный пример параметра.
- DI уменьшает дублирование, но требует контроля состояния и документации расширений.
Похожие материалы
Фон таблицы в Microsoft Word: шаги и советы
Редактирование ночного неба в Lightroom Mobile
Скачивание контента BBC iPlayer для офлайн
Эффективное расписание для учёбы
Как обойти интернет‑цензуру: DNS, VPN и Tor