JUnit5
Модуль предоставляет расширение для JUnit 5, которое позволяет тестировать приложение через тот же граф компонентов, что используется во время работы приложения.
Расширение Kora для JUnit 5 предназначено для компонентного и интеграционного тестирования исходного кода, который впоследствии будет работать в реальном приложении.
В тесте используется контейнер зависимостей основного приложения: его можно ограничить нужными компонентами,
расширить тестовыми компонентами или заменить отдельные части заглушками.
Модуль позволяет проводить:
Компонентные тесты— тестирование одного компонента.Межкомпонентные тесты— тестирование нескольких компонентов и их взаимодействия друг с другом.Интеграционные тесты— тестирование компонентов и взаимодействия с внешними системами.
Все типы расширения находятся в пакете io.koraframework.test.extension.junit5.
Рекомендуется дополнительно тестировать артефакт сервиса, упакованный в итоговый образ, как черный ящик с помощью библиотеки Testcontainers.
Пошаговый разбор перед справочным описанием смотрите в разделах Компонентное тестирование, Интеграционное тестирование и Тестирование черного ящика.
Подключение¶
Зависимость build.gradle:
testImplementation platform("org.junit:junit-bom:6.1.3")
testImplementation "org.junit.jupiter:junit-jupiter"
testImplementation "io.koraframework:test-junit5"
Обработчик аннотаций Kora для тестовых исходников build.gradle:
Настройка платформы JUnit build.gradle:
Зависимость build.gradle.kts:
testImplementation(platform("org.junit:junit-bom:6.1.3"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("io.koraframework:test-junit5")
Обработчик символов Kora для тестовых исходников build.gradle.kts:
kspTest требуется только тогда, когда тестовые исходники объявляют собственное @KoraApp, смотрите Тестовый граф.
Тесту, который использует граф приложения из основных исходников, он не нужен.
Настройка платформы JUnit build.gradle.kts:
Использование¶
Примеры будут показаны применительно к такому приложению:
Тест¶
Чтобы включить расширение Kora, пометьте тестовый класс аннотацией @KoraAppTest.
Аннотация подключает расширение JUnit 5, находит сгенерированный граф указанного приложения @KoraApp и подготавливает контейнер зависимостей для теста.
Параметры аннотации @KoraAppTest:
value— класс, помеченный@KoraApp, граф компонентов которого будет использоваться в тесте (обязательный, без значения по умолчанию).components— дополнительные классы компонентов, которые должны присутствовать в тестовом графе помимо компонентов, найденных через@TestComponent(по умолчанию:{}).modules— интерфейсы модулей, компоненты фабричных методов которых должны присутствовать в тестовом графе помимо компонентов, найденных через@TestComponent(по умолчанию:{}).
В modules можно указывать только интерфейсы, иначе расширение завершит тест ошибкой конфигурации.
В интерфейсе модуля на Java фабричными методами считаются только default-методы, в интерфейсе модуля на Kotlin — все методы.
Если требуется протестировать весь граф, внедрите KoraAppGraph или не ограничивайте граф отдельными компонентами @TestComponent.
Компонент¶
Для внедрения и выбора компонентов для тестирования используйте аннотацию @TestComponent.
Она позволяет внедрять компоненты в аргументы тестовых методов, в конструктор и/или в поля тестового класса, а также ограничивает контейнер зависимостей этими компонентами.
Все компоненты, перечисленные в полях теста и/или в аргументах метода/конструктора и помеченные @TestComponent, будут внедрены как зависимости в рамках теста.
Тестовый контейнер зависимостей будет ограничен этими компонентами и их зависимостями.
Важно, что компоненты внутри теста должны использоваться хотя бы одним @Root компонентом, который также указан в рамках теста.
Пример теста, где компоненты внедряются в поля:
Поля для внедрения не должны быть static или final, иначе расширение завершит тест ошибкой конфигурации.
В Kotlin это означает поле lateinit var, а не val.
Пример теста, где компоненты внедряются в конструктор:
Пример теста, где компоненты внедряются в аргументы метода:
Если компонент предоставляется в графе как обертка Wrapped<T>, тест запрашивает T: расширение разворачивает такой компонент перед внедрением.
Сама обертка внедряется только тогда, когда объявленный тип — это тип обертки.
Правила внедрения¶
Компоненты можно внедрять тремя способами: в поле тестового класса, в конструктор или в параметр тестового метода. Выбранная форма влияет на то, когда расширение Kora может получить доступ к экземпляру тестового класса и какие дополнительные механизмы доступны.
- Поля подходят для большинства тестов и совместимы с
KoraAppTestConfigModifier,KoraAppTestGraphModifier,PER_METHODиPER_CLASS. - Внедрение через конструктор удобно для неизменяемых полей, но несовместимо с
KoraAppTestConfigModifierиKoraAppTestGraphModifier, поскольку расширению нужен экземпляр тестового класса для вызоваconfig()илиgraph(), тогда как этот экземпляр еще создается во время внедрения через конструктор. - Параметры метода удобны для зависимостей, локальных для конкретного теста; в режиме
PER_METHODграф включает параметры текущего метода, а в режимеPER_CLASSрасширение заранее собирает параметры@TestComponentиз всех методов класса. - Если используется внедрение через конструктор,
@TestComponent,@Mock,@Spy,@MockKили@SpyKнельзя также внедрять в параметры тестового метода. - В режиме
PER_CLASS@Mock/@MockKнельзя внедрять в параметры тестового метода, поскольку заглушки уровня метода живут меньше, чем общий граф тестового класса. - Один и тот же элемент нельзя одновременно объявить как обычный
@TestComponent, заглушку (mock) и шпион (spy): расширение завершит тест ошибкой конфигурации.
Если тесту нужны KoraAppTestConfigModifier или KoraAppTestGraphModifier, используйте внедрение через поля или параметры метода.
Если требуется внедрение через конструктор, лучше вынести изменение конфигурации и графа в отдельное тестовое @KoraApp или подключаемый модуль.
Тег¶
Чтобы внедрить зависимость/заглушку, помеченную @Tag, необходимо указать соответствующую аннотацию @Tag рядом с аргументом для внедрения:
Аннотация @Tag принимает один класс тега.
Подходит и собственная аннотация, помеченная @Tag: расширение считывает тег из такой мета-аннотации.
Точка внедрения без @Tag соответствует только компонентам без тега,
поэтому граф, где один и тот же тип зарегистрирован и с тегом, и без него, не становится неоднозначным.
Граф приложения¶
Если тесту нужен прямой доступ к подготовленному графу, внедрите KoraAppGraph в поле, конструктор или аргумент тестового метода.
Он может получить один или несколько компонентов по типу, а также учитывать @Tag.
Основные методы KoraAppGraph:
getFirst(Type type)/getFirst(Class<T> type)— возвращают первый найденный компонент без тега илиnull.getFirst(Type type, Class<?> tag)/getFirst(Class<T> type, Class<?> tag)— возвращают первый компонент с указанным тегом илиnull.findFirst(...)— возвращаетOptional<T>вместоnull.getAll(...)— возвращает все компоненты указанного типа; перегрузки без тега используютTag.Anyи поэтому возвращают в том числе компоненты с любыми тегами.
Для компонента с параметрами обобщения описывайте тип через TypeRef, поскольку сырой Class не соответствует параметризованному узлу графа:
Точно так же можно внедрить инициализированный Graph приложения, если тесту нужен низкоуровневый контракт контейнера.
KoraAppGraph и Graph нельзя использовать как цель для @Mock, @Spy, @MockK или @SpyK, поскольку это служебные объекты тестового расширения, а не компоненты приложения.
Заглушка¶
Для создания заглушки компонента в Java в рамках теста предлагается использовать аннотации из библиотеки Mockito совместно с аннотацией @TestComponent.
Требуется добавить библиотеку Mockito как зависимость в build.gradle:
Kora компилируется под Java 25, поэтому библиотека заглушек должна приносить версию Byte Buddy, которая понимает class-файлы Java 25.
Устаревший mockito-core падает во время выполнения с IllegalArgumentException: Java 25 (69) is not supported by the current version of Byte Buddy.
Важно, предполагается, что MockitoExtension использоваться не будет и будет отключено, его нельзя совмещать вместе с @KoraAppTest.
Поддерживаются аннотации @Mock и @Spy, а также все параметры этих аннотаций. Рекомендуется подробнее ознакомиться с тем, как работают эти аннотации, в официальной документации библиотеки Mockito.
Аннотация @Mock позволяет сделать заглушку класса
помеченного компонента и управлять поведением его методов с помощью Mockito, либо методы будут возвращать значения по умолчанию: void, значения по умолчанию для примитивов, пустые коллекции и null для всех остальных объектов.
Компонент-заглушка будет внедрен как зависимость в аргументы и/или поля тестового класса и во все компоненты, которым он требовался как зависимость. Все зависимые компоненты, которые больше нигде в рамках теста не требуются, будут исключены как ненужные.
Пример теста с использованием компонента @Mock и внедрением заглушки в поле:
@KoraAppTest(Application.class)
class SomeTests {
@Mock
@TestComponent
private Supplier<String> component1;
@BeforeEach
void mock() {
Mockito.when(component1.get()).thenReturn("?");
}
@Test
void example() {
assertEquals("?", component1.get());
}
}
Аннотация @Spy позволяет сделать шпион-фасад реализации класса компонента из контейнера зависимостей, который по умолчанию будет иметь исходное поведение методов компонента, но, как и в случае с заглушками, их поведение можно переопределить.
Компонент-шпион будет внедрен как зависимость в аргументы и/или поля тестового класса и во все компоненты, которым он требовался как зависимость.
Пример теста с использованием компонента @Spy и внедрением шпиона в аргумент метода:
@KoraAppTest(Application.class)
class SomeTests {
@Test
void example(@Spy @TestComponent Supplier<String> component1) {
Mockito.when(component1.get()).thenReturn("?");
assertEquals("?", component1.get());
}
}
Также можно сделать шпион из значения поля тестового класса.
Компонент-шпион будет внедрен как зависимость в аргументы и/или поля тестового класса и во все компоненты, которым он требовался как зависимость. Все зависимые компоненты, которые больше нигде в рамках теста не требуются, будут исключены как ненужные.
Пример теста с использованием компонента-шпиона @Spy:
Для создания заглушек компонентов в Kotlin предлагается использовать аннотации из библиотеки MockK совместно с аннотацией @TestComponent.
Требуется подключить библиотеку MockK как зависимость в build.gradle.kts:
Kora компилируется под Java 25, а MockK версии ниже 1.14.9 приносит Byte Buddy 1.14.x, который не умеет преобразовывать такие классы.
В этом случае граф не инициализируется, а в логе появляется Failed to transform class ... Java 25 (69) is not supported.
Важно, предполагается, что MockkExtension использоваться не будет и будет отключено, его нельзя совмещать вместе с @KoraAppTest.
Поддерживаются аннотации @MockK и @SpyK, а также все параметры этих аннотаций.
Указывайте их как @field:MockK и @field:SpyK, чтобы аннотация попала на поле; аннотация на уровне свойства также работает, если в тестовом classpath есть kotlin-reflect.
Контракты Kora синхронные, поэтому тестам не нужны runTest или runBlocking, а поведение заглушек описывается через every, а не coEvery.
При желании также можно использовать Mockito. Для более подробного описания того, как работают Kora и Mockito, следует прочитать вкладку Java этого раздела. Для улучшения взаимодействия между Mockito и Kotlin можно использовать библиотеку Mockito Kotlin.
testImplementation("org.mockito:mockito-core:5.18.0")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
mockito-kotlin тянет собственную более старую версию mockito-core, поэтому указывайте mockito-core явно рядом, иначе Byte Buddy не примет class-файлы Java 25.
Важно, предполагается, что MockitoExtension использоваться не будет и будет отключено, его нельзя совмещать вместе с @KoraAppTest.
Аннотация @MockK позволяет сделать заглушку класса
помеченного компонента и управлять поведением его методов с помощью MockK.
Компонент-заглушка будет внедрен как зависимость в аргументы и/или поля тестового класса и во все компоненты, которым он требовался как зависимость. Все зависимые компоненты, которые больше нигде в рамках теста не требуются, будут исключены как ненужные.
Пример теста с использованием компонента @MockK и внедрением заглушки:
@KoraAppTest(Application::class)
class SomeTests {
@field:MockK
@TestComponent
lateinit var component1: Supplier<String>
@BeforeEach
fun mock() {
every { component1.get() } returns "?"
}
@Test
fun example() {
assertEquals("?", component1.get())
}
}
Аннотация @SpyK позволяет сделать шпион-фасад реализации класса компонента из контейнера зависимостей, который по умолчанию будет иметь исходное поведение методов компонента, но, как и в случае с заглушками, их поведение можно переопределить.
Компонент-шпион будет внедрен как зависимость в аргументы и/или поля тестового класса и во все компоненты, которым он требовался как зависимость.
Пример теста с использованием компонента @SpyK и встраиванием шпиона в аргумент метода:
@KoraAppTest(Application::class)
class SomeTests {
@Test
fun example(@SpyK @TestComponent component1: Supplier<String>) {
every { component1.get() } returns "?"
assertEquals("?", component1.get())
}
}
Также можно сделать шпион из значения поля тестового класса.
Компонент-шпион будет внедрен как зависимость в аргументы и/или поля тестового класса и во все компоненты, которым он требовался как зависимость. Все зависимые компоненты, которые больше нигде в рамках теста не требуются, будут исключены как ненужные.
Пример теста с использованием компонента-шпиона @SpyK:
Строгость заглушек¶
Заглушки Mockito можно проверять с помощью аннотации @MockitoStrictness из пакета io.koraframework.test.extension.junit5.mockito.
Она задает уровень проверки для заглушек Mockito, созданных расширением Kora в рамках тестового класса.
Расширение ведет себя аналогично MockitoSession: после завершения теста оно передает созданные заглушки на проверку Mockito и сообщает о неиспользованных или подозрительных настройках поведения.
Если @MockitoStrictness не указана, Kora использует Strictness.WARN: тест не падает, но в лог записываются предупреждения.
Поддерживаемые уровни:
Strictness.WARN— значение по умолчанию; записывает предупреждения в лог и не приводит к падению теста.Strictness.STRICT_STUBS— строгий режим; неиспользованная настройка поведения приводит к падению теста, например сUnnecessaryStubbingException.Strictness.LENIENT— мягкий режим; отключает проверки неиспользованных настроек поведения.
Если у конкретной @Mock есть собственный параметр strictness, он применяется к настройкам этой заглушки.
@MockitoStrictness удобна как общий уровень для всего тестового класса, чтобы не дублировать настройку на каждой заглушке.
В примере выше Mockito.when(component1.get()).thenReturn("?") должно быть использовано тестом.
Если убрать вызов component1.get() из тестового метода, Strictness.STRICT_STUBS приведет к падению теста.
Для Kotlin с Mockito Kotlin действует тот же механизм, поскольку проверку выполняет Mockito.
@MockitoStrictness не применяется к заглушкам MockK.
Тестовый граф¶
Иногда в рамках тестов может потребоваться использовать расширенный контейнер зависимостей. Например, тестовое приложение может расширять основное приложение и добавлять компоненты, которые нужны только в тестах.
Такой подход полезен, когда у вас есть разные приложения Read API и Write API с общими компонентами, которые могут потребоваться при тестировании одного и другого. Или же вам могут понадобиться какие-то функции сохранения/удаления/обновления исключительно для тестирования в качестве быстрой тестовой утилиты.
Рекомендация
Настоятельно рекомендуется тестировать приложения как черный ящик и полагаться на этот подход как на основной источник истины и корректности приложения.
Приложение может работать по-разному в зависимости от флагов JVM, базового образа и нативных библиотек, различий между частичными и полными конфигурациями, различий в преобразовании на точках входа приложения, использования реестров схем и так далее. Только prod-ready образ может гарантировать максимально близкое к реальному окружение тестирования.
Представим, что приложение выглядит так:
В тестах можно создать отдельное тестовое @KoraApp, которое расширяет основное приложение, и использовать этот граф.
Для этого сценария требуется сгенерированный субмодуль основного приложения: без него тестовое приложение не сможет унаследовать и подключить компоненты основного графа.
Сначала включите параметр, который создает субмодуль основного приложения, в build.gradle:
Затем требуется создать расширенный тестовый граф приложения в каталоге тестовых исходников.
Не забудьте пометить компоненты как @Root, поскольку они, скорее всего, никем не используются,
кроме тестов, и иначе не будут включены в граф:
Чтобы граф тестового приложения был сгенерирован, нужно добавить обработчики как тестовые зависимости в build.gradle:
Может потребоваться исключить сканирование сгенерированных Kora классов средствами JUnit (иногда возникает ошибка при поиске тестов):
Теперь вы можете использовать расширенный граф приложения в своих тестах:
Если сгенерированный класс графа найти не удалось, расширение сообщает Cannot find generated Kora application graph
и перечисляет, что проверить: обработчик для тестового набора исходников и kora.app.submodule.enabled для основного приложения.
Параметр modules аннотации @KoraAppTest не подключает новые модули к графу, он объявляет, какие компоненты должны присутствовать в ограниченном тестовом графе.
Указанный модуль уже должен принадлежать графу тестируемого @KoraApp: либо интерфейс приложения его расширяет,
либо модуль помечен @Module и компилируется вместе с приложением, включая тестовые исходники, когда само тестовое приложение объявлено в src/test:
@Module
public interface TestModule {
@Root
@Tag(TestModule.class)
default String testOnlyComponent() {
return "module";
}
}
@KoraAppTest(value = TestApplication.class, modules = TestModule.class)
class SomeTests {
@Test
void test(@Tag(TestModule.class) @TestComponent String component) {
assertEquals("module", component);
}
}
@Module
interface TestModule {
@Root
@Tag(TestModule::class)
fun testOnlyComponent(): String {
return "module"
}
}
@KoraAppTest(value = TestApplication::class, modules = [TestModule::class])
class SomeTests {
@Test
fun test(@Tag(TestModule::class) @TestComponent component: String) {
assertEquals("module", component)
}
}
Итого:
kora.app.submodule.enabled=trueнужен, когда тестовое@KoraAppрасширяет основное@KoraApp.- Обработчик должен быть подключен к тестовому набору исходников как
testAnnotationProcessorв Java иkspTestв Kotlin, когда тестовые исходники объявляют собственное@KoraApp. @KoraAppTest(modules = ...)подходит для случаев, когда компоненты уже подключенного модуля должны присутствовать в ограниченном тестовом графе.- Компоненты, которые должны появиться в ограниченном тестовом графе, все равно должны быть достижимы из
@TestComponent,componentsилиKoraAppGraph.
Конфигурация теста¶
По умолчанию будет использоваться базовая конфигурация, как и в случае запуска реального приложения.
Чтобы изменить или добавить конфигурацию в рамках тестов, тестовый класс должен реализовать KoraAppTestConfigModifier,
а метод config() должен возвращать KoraConfigModification.
KoraAppTestConfigModifier нельзя использовать вместе с внедрением компонентов в конструктор тестового класса:
расширению нужно получить изменение конфигурации до создания тестового графа, а для этого экземпляр теста должен уже существовать.
Значения, переданные через withSystemProperty, устанавливаются как системные свойства JVM только на время построения тестового графа и затем восстанавливаются.
Переменные окружения¶
Если тесту нужно использовать конфигурацию по умолчанию, которая использовалась бы при запуске приложения,
и требуется лишь подставить переменные окружения, можно воспользоваться механизмом SystemProperty в KoraConfigModification:
Предположим, есть такая конфигурация application.conf:
Чтобы использовать такой конфиг и передать только переменные окружения, нужно вернуть такой KoraConfigModification:
@KoraAppTest(Application.class)
class SomeTests implements KoraAppTestConfigModifier {
@Override
public KoraConfigModification config() {
return KoraConfigModification
.ofSystemProperty("POSTGRES_JDBC_URL", "jdbc:postgresql://localhost:5432/postgres")
.withSystemProperty("POSTGRES_USER", "postgres")
.withSystemProperty("POSTGRES_PASS", "postgres");
}
}
@KoraAppTest(Application::class)
class SomeTests : KoraAppTestConfigModifier {
override fun config(): KoraConfigModification {
return KoraConfigModification
.ofSystemProperty("POSTGRES_JDBC_URL", "jdbc:postgresql://localhost:5432/postgres")
.withSystemProperty("POSTGRES_USER", "postgres")
.withSystemProperty("POSTGRES_PASS", "postgres")
}
}
Если нужно передать сразу несколько значений, используйте withSystemProperties(Map<String, String>):
@KoraAppTest(Application.class)
class SomeTests implements KoraAppTestConfigModifier {
@Override
public KoraConfigModification config() {
return KoraConfigModification
.ofSystemProperty("POSTGRES_JDBC_URL", "jdbc:postgresql://localhost:5432/postgres")
.withSystemProperties(Map.of(
"POSTGRES_USER", "postgres",
"POSTGRES_PASS", "postgres"
));
}
}
@KoraAppTest(Application::class)
class SomeTests : KoraAppTestConfigModifier {
override fun config(): KoraConfigModification {
return KoraConfigModification
.ofSystemProperty("POSTGRES_JDBC_URL", "jdbc:postgresql://localhost:5432/postgres")
.withSystemProperties(
mapOf(
"POSTGRES_USER" to "postgres",
"POSTGRES_PASS" to "postgres"
)
)
}
}
Файл конфигурации¶
Пример предоставления конфигурации в виде файла, файл ищется в каталоге resources тестов:
Текст конфигурации¶
Пример добавления конфигурации в виде строки выглядел бы так, в этом случае будет использоваться только эта конфигурация без каких-либо файлов конфигурации:
Подстановка в конфигурации¶
Подстановка переменных окружения, показанная в разделе Переменные окружения, также работает со встроенной конфигурацией:
объявите плейсхолдеры ${ENV} прямо внутри конфигурации ofString(...) и разрешите их через цепочку вызовов withSystemProperty(...).
Это удобно, когда вся конфигурация описана в тесте, но некоторые значения (порты, хосты, учетные данные) известны только во время выполнения:
@KoraAppTest(Application.class)
class SomeTests implements KoraAppTestConfigModifier {
@Override
public KoraConfigModification config() {
return KoraConfigModification.ofString("""
myconfig {
myinnerconfig {
first = ${ENV_FIRST}
second = ${ENV_SECOND}
}
}
""")
.withSystemProperty("ENV_FIRST", "1")
.withSystemProperty("ENV_SECOND", "2");
}
}
В необрабатываемой строке Kotlin символ $ необходимо экранировать как ${'$'}, иначе он будет воспринят как шаблон строки:
@KoraAppTest(Application::class)
class SomeTests : KoraAppTestConfigModifier {
override fun config(): KoraConfigModification {
return KoraConfigModification.ofString(
"""
myconfig {
myinnerconfig {
first = ${'$'}{ENV_FIRST}
second = ${'$'}{ENV_SECOND}
}
}
""".trimIndent()
)
.withSystemProperty("ENV_FIRST", "1")
.withSystemProperty("ENV_SECOND", "2")
}
}
Testcontainers¶
Распространенное применение KoraAppTestConfigModifier — интеграция с Testcontainers:
тест запускает контейнер и передает его значения подключения времени выполнения в конфигурацию через config().
Testcontainers назначает случайный порт хоста при каждом запуске, поэтому значения нельзя жестко зашивать — они объявляются как плейсхолдеры ${...} во встроенной конфигурации
и заполняются из геттеров контейнера через withSystemProperty(...).
Поскольку config() выполняется до построения тестового графа, конфигурация готова до создания любого компонента.
По той же причине KoraAppTestConfigModifier несовместим с внедрением через конструктор: используйте внедрение через поле или параметр метода, как показано ниже.
Добавьте зависимости Testcontainers в build.gradle:
testImplementation "org.testcontainers:testcontainers-junit-jupiter:2.0.5"
testImplementation "org.testcontainers:testcontainers-postgresql:2.0.5"
@Testcontainers
@KoraAppTest(Application.class)
class SomeIntegrationTests implements KoraAppTestConfigModifier {
@Container
static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine");
@TestComponent
private SomeService service;
@Override
public KoraConfigModification config() {
return KoraConfigModification.ofString("""
jdbc {
jdbcUrl = ${POSTGRES_JDBC_URL}
username = ${POSTGRES_USER}
password = ${POSTGRES_PASS}
poolName = "kora-test"
}
""")
.withSystemProperty("POSTGRES_JDBC_URL", POSTGRES.getJdbcUrl())
.withSystemProperty("POSTGRES_USER", POSTGRES.getUsername())
.withSystemProperty("POSTGRES_PASS", POSTGRES.getPassword());
}
@Test
void example() {
// interact with the service backed by the container
}
}
Добавьте зависимости Testcontainers в build.gradle.kts:
testImplementation("org.testcontainers:testcontainers-junit-jupiter:2.0.5")
testImplementation("org.testcontainers:testcontainers-postgresql:2.0.5")
@Testcontainers
@KoraAppTest(Application::class)
class SomeIntegrationTests : KoraAppTestConfigModifier {
companion object {
@Container
@JvmStatic
val POSTGRES = PostgreSQLContainer("postgres:16-alpine")
}
@TestComponent
lateinit var service: SomeService
override fun config(): KoraConfigModification {
return KoraConfigModification.ofString(
"""
jdbc {
jdbcUrl = ${'$'}{POSTGRES_JDBC_URL}
username = ${'$'}{POSTGRES_USER}
password = ${'$'}{POSTGRES_PASS}
poolName = "kora-test"
}
""".trimIndent()
)
.withSystemProperty("POSTGRES_JDBC_URL", POSTGRES.jdbcUrl)
.withSystemProperty("POSTGRES_USER", POSTGRES.username)
.withSystemProperty("POSTGRES_PASS", POSTGRES.password)
}
@Test
fun example() {
// interact with the service backed by the container
}
}
Показанная выше секция jdbc — это конфигурация подключения JDBC, та же самая секция, которую приложение использует во время работы.
Полный разбор — зависимости, тестовое @KoraApp, миграции и настройка репозитория — смотрите в руководстве Интеграционное тестирование.
Изменение контейнера¶
Чтобы добавить, заменить или программно создать заглушки в контейнере приложения без аннотаций, реализуйте KoraAppTestGraphModifier
и верните KoraGraphModification из метода graph().
KoraAppTestGraphModifier нельзя использовать вместе с внедрением компонентов в конструктор тестового класса:
расширению нужно получить изменение графа до создания графа и внедрения компонентов.
KoraGraphModification поддерживает следующие операции:
addComponent(...)— добавляет новый компонент в тестовый граф.replaceComponent(...)— заменяет существующий компонент. СSupplierзависимости заменяемого компонента не создаются, сFunction<KoraAppGraph, T>они остаются в графе, поскольку замена строится из уже инициализированных компонентов графа.mockComponent(...)— заменяет существующий компонент заглушкой, зависимости заменяемого компонента не создаются.
У каждого из этих методов есть перегрузка, принимающая тег как Class<?> между типом и фабрикой, для компонентов, объявленных с @Tag.
Если ни один компонент не соответствует типу и тегу, расширение завершает тест ошибкой Cannot replace Kora component.
Добавление¶
Пример добавления компонента в граф:
@KoraAppTest(value = Application.class)
class SomeTests implements KoraAppTestGraphModifier {
@Override
public KoraGraphModification graph() {
return KoraGraphModification.create()
.addComponent(TypeRef.of(Supplier.class, Integer.class), () -> (Supplier<Integer>) () -> 1);
}
@Test
void example(@TestComponent Supplier<Integer> supplier) {
assertEquals(1, supplier.get());
}
}
@KoraAppTest(value = Application::class)
class SomeTests : KoraAppTestGraphModifier {
override fun graph(): KoraGraphModification {
return KoraGraphModification.create()
.addComponent(TypeRef.of(Supplier::class.java, Int::class.javaObjectType), Supplier { Supplier { 1 } })
}
@Test
fun example(@TestComponent supplier: Supplier<Int>) {
assertEquals(1, supplier.get())
}
}
В Kotlin фабрика оборачивается в явный SAM-конструктор Supplier { ... }, поскольку у addComponent есть также перегрузка с Function<KoraAppGraph, T>
и голая лямбда была бы неоднозначной. Параметры обобщений в графе — это обертки, поэтому используется Int::class.javaObjectType, а не Int::class.java, который является примитивом int.
В случае, когда требуется добавить компоненты с использованием реального компонента из графа, это также доступно через другую сигнатуру метода:
@KoraAppTest(value = Application.class)
class SomeTests implements KoraAppTestGraphModifier {
@Override
public KoraGraphModification graph() {
return KoraGraphModification.create()
.addComponent(TypeRef.of(Supplier.class, Long.class),
(graph) -> {
final Supplier<String> existingComponent = (Supplier<String>) graph.getFirst(TypeRef.of(Supplier.class, String.class));
return (Supplier<Long>) () -> Long.parseLong(existingComponent.get());
});
}
@Test
void example(@TestComponent Supplier<Long> supplier) {
assertEquals(1L, supplier.get());
}
}
@KoraAppTest(value = Application::class)
class SomeTests : KoraAppTestGraphModifier {
override fun graph(): KoraGraphModification {
return KoraGraphModification.create()
.addComponent(TypeRef.of(Supplier::class.java, Long::class.javaObjectType))
{ graph ->
val existingComponent = graph.getFirst(TypeRef.of(Supplier::class.java, String::class.java))
as Supplier<String>
Supplier { existingComponent.get().toLong() }
}
}
@Test
fun example(@TestComponent supplier: Supplier<Long>) {
assertEquals(1L, supplier.get())
}
}
Замена¶
Пример замены компонента в контейнере зависимостей, этот механизм также можно использовать для создания собственных заглушек:
@KoraAppTest(value = Application.class)
class SomeTests implements KoraAppTestGraphModifier {
@Override
public KoraGraphModification graph() {
return KoraGraphModification.create()
.replaceComponent(TypeRef.of(Supplier.class, String.class), Supplier.class, () -> (Supplier<String>) () -> "?");
}
@Test
void example(@Tag(Supplier.class) @TestComponent Supplier<String> supplier) {
assertEquals("?", supplier.get());
}
}
@KoraAppTest(value = Application::class)
class SomeTests : KoraAppTestGraphModifier {
override fun graph(): KoraGraphModification {
return KoraGraphModification.create()
.replaceComponent(TypeRef.of(Supplier::class.java, String::class.java), Supplier::class.java, Supplier { Supplier { "?" } })
}
@Test
fun example(@Tag(Supplier::class) @TestComponent supplier: Supplier<String>) {
assertEquals("?", supplier.get())
}
}
В случае, когда требуется заменить компоненты с использованием реального компонента из графа, это также доступно через другую сигнатуру метода:
@KoraAppTest(value = Application.class)
class SomeTests implements KoraAppTestGraphModifier {
@Override
public KoraGraphModification graph() {
return KoraGraphModification.create()
.replaceComponent(TypeRef.of(Supplier.class, String.class), Supplier.class,
(graph) -> {
final Supplier<String> existingComponent = (Supplier<String>) graph.getFirst(TypeRef.of(Supplier.class, String.class));
return (Supplier<String>) () -> existingComponent.get() + "2";
});
}
@Test
void example(@Tag(Supplier.class) @TestComponent Supplier<String> supplier) {
assertEquals("12", supplier.get());
}
}
@KoraAppTest(value = Application::class)
class SomeTests : KoraAppTestGraphModifier {
override fun graph(): KoraGraphModification {
return KoraGraphModification.create()
.replaceComponent(TypeRef.of(Supplier::class.java, String::class.java), Supplier::class.java)
{ graph ->
val existingComponent = graph.getFirst(TypeRef.of(Supplier::class.java, String::class.java))
as Supplier<String>
Supplier { existingComponent.get() + "2" }
}
}
@Test
fun example(@Tag(Supplier::class) @TestComponent supplier: Supplier<String>) {
assertEquals("12", supplier.get())
}
}
Замена выше читает компонент без тега и заменяет компонент с тегом Supplier:
фабрика никогда не должна запрашивать тот самый компонент, который она заменяет, иначе инициализация графа зациклится.
Программная заглушка¶
Если компонент нужно заменить именно как заглушку, используйте mockComponent(...).
Как и форма replaceComponent(...) с Supplier, этот метод сообщает расширению, что реальные зависимости заменяемого компонента не нужны и могут быть исключены из тестового графа.
@KoraAppTest(value = Application.class)
class SomeTests implements KoraAppTestGraphModifier {
@Override
public KoraGraphModification graph() {
return KoraGraphModification.create()
.mockComponent(TypeRef.of(Supplier.class, String.class), () -> Mockito.mock(Supplier.class));
}
@Test
void example(@TestComponent Supplier<String> supplier) {
Mockito.when(supplier.get()).thenReturn("?");
assertEquals("?", supplier.get());
}
}
@KoraAppTest(value = Application::class)
class SomeTests : KoraAppTestGraphModifier {
override fun graph(): KoraGraphModification {
return KoraGraphModification.create()
.mockComponent(TypeRef.of(Supplier::class.java, String::class.java), Supplier { mockk<Supplier<String>>() })
}
@Test
fun example(@TestComponent supplier: Supplier<String>) {
every { supplier.get() } returns "?"
assertEquals("?", supplier.get())
}
}
Инициализация¶
По умолчанию JUnit 5 использует TestInstance.Lifecycle.PER_METHOD, поэтому Kora создает и очищает тестовый граф для каждого тестового метода.
Если контейнер должен инициализироваться один раз для всего тестового класса, пометьте тестовый класс аннотацией @TestInstance(TestInstance.Lifecycle.PER_CLASS):
С PER_CLASS один экземпляр графа используется всеми тестовыми методами класса, а очистка выполняется после завершения всего класса.
Это ускоряет тяжелые интеграционные тесты, но с изменяемым состоянием компонентов и заглушек нужно обращаться аккуратнее.
Перед каждым тестовым методом расширение сбрасывает все заглушки и шпионы общего графа, поэтому настройку поведения нужно объявлять в каждом тестовом методе.
Ограничения жизненного цикла:
- Когда компоненты внедряются в конструктор,
@TestComponentили заглушки нельзя также внедрять в параметры тестового метода. - Когда компоненты внедряются в конструктор, нельзя использовать
KoraAppTestConfigModifierиKoraAppTestGraphModifier. - В режиме
PER_CLASS@Mock/@MockKнельзя внедрять в параметры тестового метода; используйте поля или конструктор. - Для классов
@Nestedнельзя использовать внедрение в поля внутреннего класса, если внешний тестовый класс работает в режимеPER_CLASS; используйте параметры метода или отдельный жизненный цикл для вложенного класса.