Kora облачно ориентированный серверный фреймворк написанный на Java для написания Java / Kotlin приложений с упором на производительность, эффективность, прозрачность сделанный выходцами из Т-Банк / Тинькофф

Kora is a cloud-oriented server-side Java framework for writing Java / Kotlin applications with a focus on performance, efficiency and transparency

Перейти к содержанию

Компонентное тестирование в Kora

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

Если в процессе захочется сверить результат, используйте готовое рабочее приложение: Kora Java Testing JUnit App.

Если в процессе захочется сверить результат, используйте готовое рабочее приложение: Kora Kotlin Testing JUnit App.

Что вы создадите

Вы создадите полноценный набор тестов, который покрывает:

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

Что понадобится

  • JDK 17 или новее
  • Gradle 7+
  • текстовый редактор или среда разработки
  • пройденное руководство HTTP-сервер

Требования

Обязательно: пройдите руководство по HTTP-серверу

Это руководство предполагает, что вы уже прошли HTTP-сервер и у вас есть рабочий проект Kora с UserService, UserController, DTO и потоком работы через репозиторий в памяти.

Если вы еще не прошли руководство по HTTP-серверу, сначала сделайте это, потому что здесь тестируется уже существующее поведение службы и контроллера, а не создается приложение с нуля.

Обзор

Тестирование приложения Kora с JUnit начинается с выбора правильной границы для вопроса, на который должен ответить тест. Тест службы может ответить, корректно ли работает бизнес-логика. Интеграционный тест может ответить, работает ли служба с настоящей инфраструктурой. Тест по принципу черного ящика может ответить, корректно ли полноценное приложение ведет себя через открытый API.

Важное отличие от обычного модульного тестирования в том, что код Kora часто определяется графом приложения. Конструкторы, сгенерированные компоненты, конфигурация и модули фреймворка влияют на поведение, поэтому тесты должны явно показывать, какую часть графа они проверяют.

Уровни тестирования

Это руководство вводит основные уровни тестирования, которые используются в руководствах Kora:

  • компонентные тесты строят часть графа Kora и заменяют выбранные зависимости подменами
  • интеграционные тесты оставляют больше настоящих компонентов и добавляют инфраструктуру, например PostgreSQL через Testcontainers
  • тесты по принципу черного ящика запускают полноценное приложение и взаимодействуют с ним только через открытые HTTP API

Эти уровни не конкурируют друг с другом. Они по-разному соотносят скорость, изоляцию и уверенность. Компонентные тесты полезны для быстрой обратной связи и проверки сфокусированного поведения. Интеграционные тесты полезны, когда важны SQL, конфигурация, миграции или настоящие клиенты. Тесты по принципу черного ящика дают самую сильную уверенность на уровне пользователя, потому что включают маршрутизацию, сериализацию, конфигурацию, связывание фреймворка и инфраструктуру.

Тестовые графы Kora

Граф Kora, создаваемый во время компиляции, является важным инструментом тестирования. @KoraAppTest может запустить граф приложения для теста, а аннотации изменения графа могут заменять компоненты, внедрять подмены или добавлять компоненты только для тестов. Так тесты остаются близкими к настоящему связыванию приложения, но при этом не заставляют каждый тест запускать всю среду выполнения.

Важная идея в том, что тест Kora часто проверяет форму графа, а не только отдельный класс. Это полезно, когда поведение зависит от внедрения зависимостей, сгенерированных компонентов, конфигурации или модулей фреймворка.

Testcontainers и реалистичность

Проверки быстрые, но они не доказывают, что SQL выполняется, миграции соответствуют коду или внешние протоколы настроены правильно. Testcontainers дает тестам изолированную настоящую инфраструктуру, например PostgreSQL, и при этом сохраняет окружение одноразовым и повторяемым.

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

Практический ход такой:

  1. добавить зависимости JUnit, Mockito для Java, MockK для Kotlin, Kora test и Testcontainers
  2. объявить тестовый граф Kora
  3. заменить выбранные зависимости подменами
  4. проверить поведение службы через компоненты, управляемые графом
  5. добавить переопределения конфигурации для тестовых сценариев
  6. подготовить проект к более глубокому интеграционному тестированию и тестированию как черный ящик

Зависимости

Добавьте следующие тестовые зависимости в build.gradle:

build.gradle
dependencies {
    // ... существующие зависимости ...

    testImplementation(platform("org.junit:junit-bom:5.14.3"))
    testImplementation(project(":guide-http-server-app"))
    testImplementation("org.junit.jupiter:junit-jupiter")
    testImplementation("ru.tinkoff.kora:http-server-undertow")

    // Тестовый фреймворк Kora с интеграцией JUnit 5
    testImplementation("ru.tinkoff.kora:test-junit5")

    // Фреймворк подмен для компонентного тестирования
    testImplementation("org.mockito:mockito-core:5.23.0")
}

test {
    useJUnitPlatform()
    testLogging {
        showStandardStreams(true)
        events("passed", "skipped", "failed")
        exceptionFormat("full")
    }
}

Добавьте следующие тестовые зависимости в build.gradle.kts:

build.gradle.kts
dependencies {
    // ... существующие зависимости ...

    testImplementation(platform("org.junit:junit-bom:5.14.3"))
    testImplementation(project(":guide-http-server-app"))
    testImplementation("org.junit.jupiter:junit-jupiter")
    testImplementation("ru.tinkoff.kora:http-server-undertow")

    // Тестовый фреймворк Kora с интеграцией JUnit 5
    testImplementation("ru.tinkoff.kora:test-junit5")

    // Фреймворк подмен для компонентного тестирования Kotlin
    testImplementation("io.mockk:mockk:1.13.11")
}

tasks.test {
    useJUnitPlatform()
    testLogging {
        showStandardStreams = true
        events("passed", "skipped", "failed")
        exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
    }
}

Компонентные тесты

Компонентный тест в Kora находится между простой проверкой отдельного класса и полноценным запуском приложения. Вы не создаете UserService руками через new и не собираете его зависимости вручную. Вместо этого тест просит Kora построить небольшой тестовый граф, достать из него нужный компонент и внедрить этот компонент в тестовый класс.

Это важно по двум причинам:

  • тест проверяет тот же способ связывания зависимостей, который используется в приложении
  • граф можно ограничить только теми компонентами, которые нужны конкретному тесту
  • такой обрезанный граф собирается очень быстро, потому что @KoraAppTest не инициализирует лишние ветки приложения
  • по умолчанию тестовый граф создается заново для каждого тестового метода, если вы явно не выбрали другой жизненный цикл JUnit

В этом руководстве мы сначала подключим настоящий UserService как тестовый компонент. Потом заменим его зависимость UserRepository моком и посмотрим, как такая подмена попадает в граф.

Тестовый компонент

Начните с самого простого варианта: попросите Kora дать тесту настоящий компонент UserService.

@KoraAppTest(Application.class) говорит JUnit-расширению Kora, какой @KoraApp использовать как источник графа. Это не означает, что тест обязательно поднимет все приложение целиком. Тестовый граф ограничивается компонентами, которые вы явно запросили через @TestComponent, и зависимостями, которые нужны этим компонентам.

@TestComponent на поле userService означает две вещи одновременно:

  • этот компонент нужно найти в графе приложения и внедрить в поле тестового класса
  • этот компонент становится одной из корневых точек тестового графа

То есть Kora начинает с UserService, смотрит его конструктор, находит нужные зависимости и добавляет только необходимую часть графа. Если UserService зависит от UserRepository, то репозиторий тоже попадет в тестовый граф. Если HTTP-сервер, контроллеры или другие компоненты не нужны для создания UserService, они не обязаны инициализироваться в таком компонентном тесте.

Создайте src/test/java/ru/tinkoff/kora/guide/testingjunit/UserServiceComponentTest.java:

package ru.tinkoff.kora.guide.testingjunit;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import org.junit.jupiter.api.Test;
import ru.tinkoff.kora.guide.httpserver.Application;
import ru.tinkoff.kora.guide.httpserver.dto.UserRequest;
import ru.tinkoff.kora.guide.httpserver.service.UserService;
import ru.tinkoff.kora.test.extension.junit5.KoraAppTest;
import ru.tinkoff.kora.test.extension.junit5.TestComponent;

@KoraAppTest(Application.class)
class UserServiceComponentTest {

    @TestComponent
    private UserService userService;

    @Test
    void createUserWithRealGraph() {
        var request = new UserRequest("John", "john@example.com");

        var result = userService.createUser(request);

        assertNotNull(result);
        assertEquals("John", result.name());
        assertEquals("john@example.com", result.email());
    }
}

Создайте src/test/kotlin/ru/tinkoff/kora/guide/testingjunit/UserServiceComponentTest.kt:

package ru.tinkoff.kora.guide.testingjunit

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Test
import ru.tinkoff.kora.guide.httpserver.Application
import ru.tinkoff.kora.guide.httpserver.dto.UserRequest
import ru.tinkoff.kora.guide.httpserver.service.UserService
import ru.tinkoff.kora.test.extension.junit5.KoraAppTest
import ru.tinkoff.kora.test.extension.junit5.TestComponent

@KoraAppTest(Application::class)
class UserServiceComponentTest {

    @TestComponent
    lateinit var userService: UserService

    @Test
    fun createUserWithRealGraph() {
        val request = UserRequest("John", "john@example.com")

        val result = userService.createUser(request)

        assertNotNull(result)
        assertEquals("John", result.name())
        assertEquals("john@example.com", result.email())
    }
}

На этом шаге нет мока. UserService настоящий, его зависимости тоже настоящие, если Kora может построить их из приложения. Такой тест полезен, когда зависимость дешевая и полностью локальная: например, репозиторий из HTTP-гайда хранит данные в памяти и не требует внешней базы данных.

Жизненный цикл тестового графа

Когда JUnit запускает тестовый класс с @KoraAppTest, расширение Kora делает несколько вещей до выполнения тестового метода:

  1. берет Application из @KoraAppTest
  2. строит тестовую версию графа приложения
  3. оставляет в графе компоненты, запрошенные через @TestComponent, и их зависимости
  4. применяет тестовые подмены, если они объявлены
  5. инициализирует нужные компоненты графа
  6. внедряет готовые компоненты в поля, конструктор или параметры тестовых методов

После этого выполняется обычный JUnit-тест. Для тестового метода userService уже является не null, а готовым компонентом из графа Kora.

Практически это означает следующее:

  • конструктор UserService вызывается Kora, а не тестом
  • зависимости UserService берутся из того же описания приложения, что и в реальном графе
  • компоненты, которые не нужны запрошенной части графа, не создаются только ради теста
  • если компонент имеет жизненный цикл, она выполняется в рамках инициализации тестового графа
  • после завершения тестового контекста Kora закрывает инициализированные ранее компоненты
  • по умолчанию такой контейнер создается с нуля для каждого тестового метода; если нужно один раз на весь класс, в документации Kora используется @TestInstance(TestInstance.Lifecycle.PER_CLASS)

Такой тест отвечает на вопрос: “может ли Kora построить нужную часть приложения, и ведет ли себя настоящий компонент правильно?” Но иногда настоящая зависимость мешает сфокусированной проверке. Например, вы хотите проверить сортировку, обработку 404 или вызов репозитория, не завися от состояния хранилища. Тогда нужна подмена.

Мок компонента

Мок — это тестовая замена настоящего компонента. Он выглядит для графа как обычный компонент нужного типа, но его поведение задается в тесте.

Конкретный фреймворк моков зависит от языка, но роль подмены остается одинаковой:

  • аннотация мок-фреймворка создает мок нужного типа
  • @TestComponent сообщает Kora, что этот мок является компонентом тестового графа
  • тип поля UserRepository говорит, какой компонент приложения нужно заменить

Самое важное: мок попадает не только в поле тестового класса. Kora также внедряет этот же мок во все компоненты графа, которым нужен UserRepository. Поэтому UserService остается настоящим, но его конструктор получает уже не настоящий in-memory репозиторий, а тестовую подмену.

Граф для такого теста можно представить так:

test field userRepository
        |
        | same mock instance
        v
UserService ---depends on--- UserRepository
   ^                         ^
   |                         |
@TestComponent          mock @TestComponent
real component          replacement component

В результате тест получает две точки контроля:

  • через userRepository можно задать ответы зависимости
  • через userService можно вызвать настоящий код службы и проверить результат

В Java используйте Mockito: @Mock создает мок UserRepository, а when(...).thenReturn(...) задает ответ для конкретного вызова зависимости.

Обновите тестовый класс:

package ru.tinkoff.kora.guide.testingjunit;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.time.LocalDateTime;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import ru.tinkoff.kora.guide.httpserver.Application;
import ru.tinkoff.kora.guide.httpserver.dto.UserResponse;
import ru.tinkoff.kora.guide.httpserver.repository.UserRepository;
import ru.tinkoff.kora.guide.httpserver.service.UserService;
import ru.tinkoff.kora.test.extension.junit5.KoraAppTest;
import ru.tinkoff.kora.test.extension.junit5.TestComponent;

@KoraAppTest(Application.class)
class UserServiceComponentTest {

    @Mock
    @TestComponent
    private UserRepository userRepository;

    @TestComponent
    private UserService userService;

    @Test
    void getUserUsesRepositoryMock() {
        var expected = new UserResponse("1", "John", "john@example.com", LocalDateTime.now());
        when(userRepository.findById("1")).thenReturn(Optional.of(expected));

        var result = userService.getUser("1");

        assertEquals(Optional.of(expected), result);
        verify(userRepository).findById("1");
    }
}

В Kotlin используйте MockK: @MockK создает мок UserRepository, а every { ... } returns ... задает ответ для конкретного вызова зависимости без экранирования ключевых слов Kotlin.

Обновите тестовый класс:

package ru.tinkoff.kora.guide.testingjunit

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.verify
import ru.tinkoff.kora.guide.httpserver.Application
import ru.tinkoff.kora.guide.httpserver.dto.UserResponse
import ru.tinkoff.kora.guide.httpserver.repository.UserRepository
import ru.tinkoff.kora.guide.httpserver.service.UserService
import ru.tinkoff.kora.test.extension.junit5.KoraAppTest
import ru.tinkoff.kora.test.extension.junit5.TestComponent
import java.time.LocalDateTime

@KoraAppTest(Application::class)
class UserServiceComponentTest {

    @MockK
    @TestComponent
    lateinit var userRepository: UserRepository

    @TestComponent
    lateinit var userService: UserService

    @Test
    fun getUserUsesRepositoryMock() {
        val expected = UserResponse("1", "John", "john@example.com", LocalDateTime.now())
        every { userRepository.findById("1") } returns expected

        val result = userService.getUser("1")

        assertEquals(expected, result)
        verify { userRepository.findById("1") }
    }
}

Теперь тест проверяет не репозиторий, а поведение UserService при заданном ответе репозитория. Это и есть основная ценность мока: вы фиксируете поведение зависимости и проверяете реакцию компонента, который находится над ней.

Что именно инициализируется

В тесте с мокнутым UserRepository граф становится меньше и понятнее:

  • UserService создается как настоящий компонент приложения
  • UserRepository заменяется тестовым моком
  • компоненты, которые нужны только настоящему UserRepository, больше не нужны и не попадают в тестовый граф
  • HTTP-сервер и контроллеры не создаются, если вы не запросили их как @TestComponent
  • тестовый класс получает ссылки на оба компонента: настоящий userService и мокнутый userRepository

Это отличается от обычного ручного компонентного теста с моками без Kora. В таком тесте вы часто пишете new UserService(userRepository) руками. В Kora-тесте это делает граф. Поэтому тест одновременно проверяет:

  • что UserService действительно является компонентом графа
  • что Kora умеет собрать его с нужной зависимостью
  • что подмена зависимости применяется в том же месте, где production-граф использовал бы настоящий компонент
  • что бизнес-логика службы работает при заданных ответах зависимости

Не используйте отдельное JUnit-расширение мок-фреймворка вместе с @KoraAppTest. Жизненным циклом моков и их внедрением в граф управляет @KoraAppTest. Если подключить второе JUnit-расширение, становится неочевидно, кто создает мок, кто сбрасывает его состояние и какой экземпляр попадает в граф.

Написание тестов

Теперь добавьте проверки основного поведения службы с подмененным репозиторием. В каждом тесте есть три части: сначала вы задаете поведение зависимости, затем вызываете настоящий UserService, а после этого проверяете результат и факт обращения к репозиторию.

Разберем первый тест. assertNotNull(result) проверяет, что служба вообще вернула ответ, а не null. assertEquals("1", result.id()), assertEquals("John", result.name()) и проверка email фиксируют контракт ответа: идентификатор пришел из репозитория, а имя и почта перенесены из запроса без искажений. Проверка вызова репозитория дополняет asserts: она проверяет уже не значение результата, а взаимодействие с зависимостью.

В примерах ниже используются стандартные assertions из JUnit Jupiter: assertEquals, assertNotNull, assertTrue, assertThrows. Для более выразительных проверок в реальных проектах можно подключить AssertJ и писать проверки в стиле assertThat(result.name()).isEqualTo("John"), assertThat(result).isNotNull() или assertThatThrownBy { ... }.

В Java-версии поведение мока задается через Mockito: when(...).thenReturn(...) говорит, что должен вернуть репозиторий при конкретном вызове. verify(userRepository).save(...) проверяет, что настоящий UserService действительно обратился к репозиторию с ожидаемыми аргументами. Это полезно, когда результат метода важен, но не менее важно убедиться, что служба использует правильную зависимость и не пропускает нужное действие.

Добавьте импорты:

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;
import ru.tinkoff.kora.guide.httpserver.dto.UserRequest;
import ru.tinkoff.kora.http.server.common.HttpServerResponseException;

Добавьте тестовые методы:

@Test
void createUser_ShouldCreateAndReturnUser() {
    var request = new UserRequest("John", "john@example.com");

    when(userRepository.save("John", "john@example.com")).thenReturn("1");

    var result = userService.createUser(request);

    assertNotNull(result);
    assertEquals("1", result.id());
    assertEquals("John", result.name());
    assertEquals("john@example.com", result.email());
    verify(userRepository).save("John", "john@example.com");
}

@Test
void getUser_ShouldReturnUserWhenExists() {
    var expected = new UserResponse("1", "John", "john@example.com", LocalDateTime.now());
    when(userRepository.findById("1")).thenReturn(Optional.of(expected));

    var result = userService.getUser("1");

    assertTrue(result.isPresent());
    assertEquals(expected, result.get());
    verify(userRepository).findById("1");
}

@Test
void getUsers_ShouldReturnPagedUsers() {
    var users = List.of(
            new UserResponse("2", "Jane", "jane@example.com", LocalDateTime.now()),
            new UserResponse("1", "John", "john@example.com", LocalDateTime.now()));
    when(userRepository.findAll()).thenReturn(users);

    var result = userService.getUsers(0, 10, "name");

    assertEquals(2, result.size());
    assertEquals("Jane", result.get(0).name());
    assertEquals("John", result.get(1).name());
    verify(userRepository).findAll();
}

@Test
void updateUser_ShouldUpdateAndReturnUserWhenExists() {
    var request = new UserRequest("John Updated", "john.updated@example.com");
    when(userRepository.update("1", request.name(), request.email())).thenReturn(true);

    var result = userService.updateUser("1", request);

    assertEquals("1", result.id());
    assertEquals("John Updated", result.name());
    verify(userRepository).update("1", request.name(), request.email());
}

@Test
void deleteUser_ShouldCallRepositoryWhenUserExists() {
    when(userRepository.deleteById("1")).thenReturn(true);

    userService.deleteUser("1");
    verify(userRepository).deleteById("1");
}

@Test
void deleteUser_ShouldThrow404WhenUserMissing() {
    when(userRepository.deleteById("missing")).thenReturn(false);

    var exception = assertThrows(HttpServerResponseException.class, () -> userService.deleteUser("missing"));

    assertEquals(404, exception.code());
    verify(userRepository).deleteById("missing");
}

В Kotlin-версии используется MockK. Поведение мока задается DSL-выражением every { ... } returns ...: внутри фигурных скобок описывается вызов зависимости, а после returns — ответ для этого вызова. Проверка взаимодействия пишется как verify { userRepository.save(...) }. Такой синтаксис хорошо ложится на Kotlin: не нужны экранированные вызовы вроде `when`, а проверяемый вызов остается обычным Kotlin-кодом внутри блока.

Добавьте импорты:

import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertThrows
import ru.tinkoff.kora.guide.httpserver.dto.UserRequest
import ru.tinkoff.kora.http.server.common.HttpServerResponseException

Добавьте тестовые методы:

@Test
fun createUserShouldCreateAndReturnUser() {
    val request = UserRequest("John", "john@example.com")

    every { userRepository.save("John", "john@example.com") } returns "1"

    val result = userService.createUser(request)

    assertNotNull(result)
    assertEquals("1", result.id())
    assertEquals("John", result.name())
    assertEquals("john@example.com", result.email())
    verify { userRepository.save("John", "john@example.com") }
}

@Test
fun getUserShouldReturnUserWhenExists() {
    val expected = UserResponse("1", "John", "john@example.com", LocalDateTime.now())
    every { userRepository.findById("1") } returns expected

    val result = userService.getUser("1")

    assertEquals(expected, result)
    verify { userRepository.findById("1") }
}

@Test
fun getUsersShouldReturnPagedUsers() {
    val users = listOf(
        UserResponse("2", "Jane", "jane@example.com", LocalDateTime.now()),
        UserResponse("1", "John", "john@example.com", LocalDateTime.now())
    )
    every { userRepository.findAll() } returns users

    val result = userService.getUsers(0, 10, "name")

    assertEquals(2, result.size)
    assertEquals("Jane", result[0].name())
    assertEquals("John", result[1].name())
    verify { userRepository.findAll() }
}

@Test
fun updateUserShouldUpdateAndReturnUserWhenExists() {
    val request = UserRequest("John Updated", "john.updated@example.com")
    every { userRepository.update("1", request.name(), request.email()) } returns true

    val result = userService.updateUser("1", request)

    assertEquals("1", result.id())
    assertEquals("John Updated", result.name())
    verify { userRepository.update("1", request.name(), request.email()) }
}

@Test
fun deleteUserShouldCallRepositoryWhenUserExists() {
    every { userRepository.deleteById("1") } returns true

    userService.deleteUser("1")
    verify { userRepository.deleteById("1") }
}

@Test
fun deleteUserShouldThrow404WhenUserMissing() {
    every { userRepository.deleteById("missing") } returns false

    val exception = assertThrows(HttpServerResponseException::class.java) {
        userService.deleteUser("missing")
    }

    assertEquals(404, exception.code())
    verify { userRepository.deleteById("missing") }
}

Интеграционные тесты

Интеграционное тестирование с настоящим PostgreSQL, TestApplication и UserServiceIntegrationPostgresTest рассматривается в отдельном руководстве: Интеграционное тестирование.

Это руководство по JUnit сосредоточено на компонентном тестировании с @KoraAppTest, @TestComponent, @Mock для Java и @MockK для Kotlin.

Переопределения конфигурации

Полные правила тестовой конфигурации Kora описаны в разделе настройки конфигурации для тестов.

Kora предоставляет мощные возможности переопределения конфигурации для разных тестовых сценариев.

Шаблоны переопределения конфигурации

Распространенные шаблоны переопределения:

  • отключать внешние службы для компонентных тестов
  • использовать базы данных в памяти для более быстрых тестов
  • переопределять настройки подключения для тестовой инфраструктуры
  • отключать кеширование или фоновые задачи во время тестирования
  • настраивать другие порты или конечные точки

Запуск тестов

Запустите тесты через Gradle:

# Запустить все тесты
./gradlew test

# Запустить с подробным выводом
./gradlew test --info

# Запустить тесты параллельно
./gradlew test --parallel

Тестовое покрытие

Проекты Kora включают встроенные отчеты о тестах и покрытии:

# Сформировать отчеты о тестах
./gradlew test

# Сформировать отчеты о покрытии
./gradlew jacocoTestReport

# Открыть HTML-отчет о покрытии
open build/jacocoHtml/index.html

Лучшие практики

Стратегия тестирования:

  1. Отдавайте приоритет тестам по принципу черного ящика: они дают наибольшую уверенность
  2. Используйте компонентные тесты для логики: быстрая обратная связь во время разработки
  3. Интеграционные тесты для инфраструктуры: проверка взаимодействия с настоящей базой данных
  4. Тестирование алгоритмов: сложная бизнес-логика со сфокусированной изоляцией, когда это нужно

Организация тестов:

  • Один тестовый класс на промышленный класс: UserServiceUserServiceComponentTest
  • Понятные имена тестов: createUser_ShouldCreateAndReturnUser
  • Структура Given-When-Then: четкие фазы теста
  • Строители тестовых данных: единообразное создание тестовых данных

Изоляция тестов:

  • Свежая база данных для каждого теста: используйте Testcontainers с жизненным циклом на каждый метод
  • Никаких зависимостей между тестами: тесты должны выполняться независимо
  • Чистое состояние: сбрасывайте состояние между тестами
  • Освобождение ресурсов: корректно освобождайте контейнеры и соединения

Соображения производительности:

  • Параллельное выполнение: запускайте тесты параллельно, когда это возможно
  • Общие контейнеры: используйте совместное использование контейнеров для более быстрого запуска
  • Выборочное тестирование: запускайте только нужные тесты во время разработки
  • Быстрая обратная связь: компонентные тесты для быстрой проверки

Итоги

Вы изучили полноценные стратегии тестирования приложений Kora:

  • Компонентные тесты: проверка взаимодействий компонентов с помощью DI-фреймворка Kora
  • Интеграционные тесты: проверка с настоящими базами данных через Testcontainers
  • Тесты по принципу черного ящика: проверка поведения полноценного приложения через HTTP API

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

Тестовый фреймворк использует внедрение зависимостей Kora для удобных подмен и настройки, Testcontainers для реалистичного тестирования инфраструктуры и стандартные шаблоны JUnit 5 для привычной структуры тестов.

Ключевые понятия

Обзор стратегии тестирования:

  • Компонентные тесты: быстрое изолированное тестирование отдельных компонентов с использованием внедрения зависимостей Kora
  • Интеграционные тесты: реалистичное тестирование с настоящей инфраструктурой через Testcontainers
  • Тесты по принципу черного ящика: сквозное тестирование поведения полноценного приложения

Тестовый фреймворк Kora:

  • @KoraAppTest: аннотация, которая запускает внедрение зависимостей Kora для тестирования
  • Шаблон TestApplication: пользовательский класс приложения для тестовой конфигурации
  • Переопределения конфигурации: переменные окружения и системные свойства для тестовой конфигурации

Интеграция Testcontainers:

  • Настоящая инфраструктура: PostgreSQL, Redis и другие службы в Docker-контейнерах
  • Автоматический жизненный цикл: контейнеры автоматически запускаются и останавливаются вместе с выполнением тестов
  • Настройка сети: автоматическое внедрение строки подключения

Лучшие практики:

  • Изоляция тестов: каждый тест выполняется в полной изоляции со свежими контейнерами
  • Быстрая обратная связь: компонентные тесты для быстрой проверки во время разработки
  • Освобождение ресурсов: автоматическая очистка контейнеров и соединений
  • Параллельное выполнение: тесты могут выполняться параллельно для ускорения

Устранение неполадок

Testcontainers не запускается:

  • Убедитесь, что Docker запущен и доступен
  • Проверьте, что зависимости Testcontainers подключены
  • Убедитесь, что имена Docker-образов указаны правильно и образы доступны

@KoraAppTest не работает:

  • Убедитесь, что обработчик аннотаций настроен в build.gradle
  • Проверьте, что интерфейс Application включает TestModule
  • Убедитесь, что тестовый класс правильно помечен @KoraAppTest

Проблемы с подключением к базе данных:

  • Убедитесь, что контейнер PostgreSQL запущен до выполнения теста
  • Проверьте конфигурацию подключения к базе данных в тестовых свойствах
  • Убедитесь, что схема базы данных соответствует определениям сущностей

Переопределения конфигурации не применяются:

  • Убедитесь, что переменные окружения заданы до выполнения тестов
  • Проверьте, что системные свойства передаются в JVM тестов
  • Убедитесь, что имена свойств конфигурации соответствуют ожиданиям приложения

Проблемы выполнения тестов:

  • Проверьте журналы тестов: в них есть подробные сообщения об ошибках
  • Убедитесь, что все зависимости правильно внедрены
  • Проверьте изоляцию тестов: между тестами не должно быть общего состояния

Что дальше?

Помощь

Если возникли проблемы: