JUnit5
Модуль предоставляет Extension
для JUnit5 который позволяет легко тестировать приложение.
Концепция JUnit 5 расширения Kora предполагает тестирование именно исходного кода который будет в итоге использоваться в бою, это подразумевает что именно контейнер зависимостей основного приложения и участвует в рамках теста, он может быть ограничен либо его части заменены заглушками если этого требуется тест.
Модуль позволяет проводить:
Компонентное тестирование
- тестирование одного компонентаМежкомпонентное тестирование
- тестирование нескольких компонент и взаимодействие друг с другомИнтеграционное тестирование
- тестирование компонент и взаимодействие с внешними системами
Рекомендуется дополнительно проводить тестирование запакованного в финальный образ артефакта сервиса, по средствам черной коробки с помощью библиотеки TestContainers.
Подключение¶
Зависимость build.gradle
:
Настроить JUnit платформу build.gradle
:
Зависимость build.gradle.kts
:
Настроить JUnit платформу build.gradle.kts
:
Использование¶
Примеры будут показаны относительно такого приложения:
Тест¶
Предполагается использовать аннотацию @KoraAppTest
для аннотирования тестового класса.
Параметры аннотации @KoraAppTest
:
value
- обязательный параметр который указывает на класс аннотированный@KoraApp
, представляющий собой граф всех зависимостей которые будут доступны в рамках теста.components
- список@Root
компонентов, которые надо инициализировать в рамках теста, указываются компоненты не объявленные в рамках теста с помощью специальной аннотации@TestComponent
.modules
- список модулей с компонентами подключенных в приложении, которые дополнительно надо включить в контейнер зависимостей в рамках теста.
Компонент¶
Для использования компонентов в рамках теста предлагается использовать аннотацию @TestComponent
которая позволяет внедрять компоненты в аргументы метода и/или поля тестового класса.
Все компоненты перечисленные в тестовых полях и/или аргументах метода/конструктора с аннотацией @TestComponent
будут внедрены как зависимости в рамках теста
и весь контейнер зависимостей будет ограничен именно этими компонентами и их зависимостями в рамках теста.
Важно что компоненты в рамках теста должны использоваться хотя бы одним @Root компонентом который также указан в рамках теста.
Пример теста, где компоненты внедряются в поля:
Пример теста, где компоненты внедряются в конструктор:
Пример теста, где компоненты внедряются в аргументы метода:
Тег¶
Для внедрения зависимости которая имеет @Tag
, требуется указать соответствующую аннотацию @Tag
рядом с внедряемым аргументом:
Заглушки¶
Для создание заглушек компонент в Java в рамках теста предлагается использовать аннотации предоставляемые библиотекой Mockito в совокупности с аннотацией @TestComponent
.
Требуется подключить библиотеку Mockito как зависимость build.gradle
:
Поддерживаются аннотациии @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
:
Поддерживаются аннотациии @MockK и @SpyK, а также все параметры этих аннотаций.
Также есть возможность при желании использовать Mockito. Для более подробного описания работы Kora и Mockito следует ознакопиться с Java вкладкой этого абзаца. Чтобы улучшить взаимодействие Mockito и Kotlin можно использовать библиотеку Mockito Kotlin.
Аннотация @MockK позволяет сделать класс заглушку
проаннотированного компонента и контролировать поведение его методов с помощью MockK
.
Компонент заглушка будет внедрен как зависимость в аргументы и/или поля тестового класса и во все компоненты которые требовали его как зависимость. Все зависимые компоненты которые больше ни где не требуются в рамках теста будут исключены за ненанобностью.
Пример теста с использованием @MockK
компонента и внедрением заглушки в поле:
@KoraAppTest(Application::class)
class SomeTests(@MockK @TestComponent val 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
компонента шпиона:
Расширенный контейнер¶
Иногда может потребоваться использовать расширенный контейнер зависимостей в рамках тестов. К примеру, тестовый контейнер приложение, расширяющий основное приложение и добавляющий некоторые компоненты из общих модулей, которые не используются в данном приложении.
Например, когда у вас есть разные приложения чтения и записи с общими компонентами, которые могут потребоваться в рамках тестирования одного и другого. Либо, вам нужны некоторые функции сохранения/удаления/обновления только для тестирования в качестве быстрой тестовой утилиты.
Рекомендация
Настоятельно Рекомендуем Тестировать приложения как черный ящик и полагаться на этот подход в качестве основного источника правды и работоспособности приложения.
Приложение может работать по разному в зависимости от флагов JVM, базового образа и нативных библиотек, отличий частичной конфигурации от полной, отличий конвертации на точках входа в приложение, использования реестров схем и так далее. Только готовый образ может гарантировать максимально приблеженную среду для тестирования.
Представим что приложение выглядит так:
В тестах можно создать граф расширяющий основное приложение и использовать уже его в рамках тестах.
Для этого в первую очередь понадобится включить опцию
для создания сабмодуля основного приложения в build.gradle
:
Затем требуется создать расширенный тестовый граф приложения:
Требуется исключить сканирование созданных Kora классов со стороны JUnit (иногда у JUnit может возникать ошибка при поиске тестов):
Теперь можно использовать расширенный граф приложения в тестах:
Настройка конфигурации¶
По умолчанию будет использоваться основная конфигурация, как и в случае запуска реального приложения.
Для изменений/добавления конфига в рамках тестов предполагается чтобы тестовый класс реализовал интерфейс KoraAppTestConfigModifier
,
где требуется реализовать метод предоставления модификации конфига KoraConfigModification
.
Запрещено использовать KoraAppTestConfigModifier
и внедрение в конструктор так как в таком случае нельзя получить конфигуацию до внедрения.
Переменные окружения¶
В случае если в рамках теста надо использовать конфигурацию по умолчанию которая использовалась бы во время работы приложения,
и требуется лишь подставить переменные окружения то можно использовать механизм SystemProperty
в KoraConfigModification
:
Предположим есть такая конфигурация application.conf
:
Тогда, чтобы использовать такой конфиг и передать в него лишь переменные окружения требуется вернуть такой KoraConfigModification
:
@KoraAppTest(Application.class)
class SomeTests implements KoraAppTestConfigModifier {
@NotNull
@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")
}
}
Файл конфигурации¶
Пример добавления конфигурации в виде файла:
Текст конфигурации¶
Пример добавления конфигурации в виде строки будет выглядеть так, в таком случае будет использоваться только эта конфигурация без каких либо файлов конфигурации:
Модификация контейнера¶
Для добавления/замены компонент в рамках контейнера приложения без аннотаций требуется реализовать интерфейс KoraAppTestGraphModifier
и
реализовать метод предоставления модификатора контейнера.
Запрещено использовать KoraAppTestGraphModifier
и внедрение в конструктор так как в таком случае нельзя получить граф до внедрения.
Добавление¶
Пример добавления компонента в граф:
@KoraAppTest(value = Application.class)
class SomeTests implements KoraAppTestGraphModifier {
@Override
public @Nonnull 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.java), Supplier { Supplier { 1 } })
}
@Test
fun example(@TestComponent supplier: Supplier<Int>) {
assertEquals(1, supplier.get())
}
}
В случае если требуется добавлять компоненты с использованием компонент из контейнера зависимостей, то это также доступно через другую сигнатуру метода:
@KoraAppTest(value = Application.class)
class SomeTests implements KoraAppTestGraphModifier {
@Override
public @Nonnull KoraGraphModification graph() {
return KoraGraphModification.create()
.addComponent(TypeRef.of(Supplier.class, String.class),
(graph) -> {
final Supplier<Integer> existingComponent = (Supplier<Integer>) graph.getFirst(TypeRef.of(Supplier.class, Integer.class));
return (Supplier<String>) () -> "1" + existingComponent.get();
});
}
@Test
void example(@TestComponent Supplier<String> supplier) {
assertEquals(1, supplier.get());
}
}
@KoraAppTest(value = Application::class)
class SomeTests : KoraAppTestGraphModifier {
@Nonnull
override fun graph(): KoraGraphModification {
return KoraGraphModification.create()
.addComponent(TypeRef.of(Supplier::class.java, String::class.java))
{ graph ->
val existingComponent = graph.getFirst(TypeRef.of(Supplier::class.java, Int::class.java))
as Supplier<Int>
Supplier { "1" + existingComponent.get() }
}
}
@Test
fun example(@TestComponent supplier: Supplier<String>) {
assertEquals(1, supplier.get())
}
}
Замена¶
Пример замены компонента в контейнере, этот механизм также можно использовать для создания собственных заглушек:
@KoraAppTest(value = Application.class)
class SomeTests implements KoraAppTestGraphModifier {
@Override
public @Nonnull KoraGraphModification graph() {
return KoraGraphModification.create()
.replaceComponent(TypeRef.of(Supplier.class, String.class), List.of(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), listOf(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 @Nonnull KoraGraphModification graph() {
return KoraGraphModification.create()
.replaceComponent(TypeRef.of(Supplier.class, Integer.class),
(graph) -> {
final Supplier<Integer> existingComponent = (Supplier<Integer>) graph.getFirst(TypeRef.of(Supplier.class, Integer.class));
return (Supplier<Integer>) () -> 1 + existingComponent.get();
});
}
@Test
void example(@TestComponent Supplier<Integer> supplier) {
assertEquals(1, supplier.get());
}
}
@KoraAppTest(value = Application::class)
class SomeTests : KoraAppTestGraphModifier {
@Nonnull
override fun graph(): KoraGraphModification {
return KoraGraphModification.create()
.replaceComponent(TypeRef.of(Supplier::class.java, Int::class.java))
{ graph ->
val existingComponent = graph.getFirst(TypeRef.of(Supplier::class.java, Int::class.java))
as Supplier<Int>
Supplier { 1 + existingComponent.get() }
}
}
@Test
fun example(@TestComponent supplier: Supplier<Int>) {
assertEquals(1, supplier.get())
}
}
Инициализация¶
В случае если требуется инициализировать контейнер один раз в рамках всего тестового класса, следует проаннотировать тестовый класс с помощью @TestInstance(TestInstance.Lifecycle.PER_CLASS)
:
Поведение по умолчанию - инициализация контейнера с нуля для каждого тестового метода.