Перейти к основному содержимому

Тестирование на Kotlin

Разработчику Архитектору

Тестирование на Kotlin

Тест — маленькая программа, которая проверяет, что ваш код ведёт себя ожидаемо. На JVM тесты для Kotlin и Java используют одни и те же инструменты (JUnit, Gradle, CI): байт-код общий, отчёты в Jenkins/GitHub Actions одинаковые.

Отличия Kotlin:

  • классы по умолчанию final — для моков удобнее MockK;
  • асинхронность через suspend — нужны runTest и coEvery;
  • Ktor поднимает сервер в памяти без реального порта наружу.

Java-аналог: JUnit 5 в Java. Корутины: Корутины в Kotlin.


Словарь терминов

ТерминПростыми словами
Unit-тестПроверка одной функции/класса в изоляции (зависимости подменяют моками).
Интеграционный тестНесколько частей вместе (HTTP-маршрут + сериализация).
Мок (mock)Поддельная реализация интерфейса: "когда вызовут findName(1) — верни "Ann"".
@TestМетод, который запускает JUnit.
runTestТестовый scope для корутин и suspend.
coEvery / coVerifyНастройка и проверка вызовов suspend-методов в MockK.

Какой инструмент когда

ЗадачаИнструмент
Логика сервиса, мок репозиторияJUnit + MockK
suspend-функцииMockK coEvery + runTest
Читаемые spec-файлыKotest
Ktor-маршрутыktor-server-test-host
Android UICompose Test / Espresso — Jetpack Compose — первый экран, Экосистема Kotlin-приложений

JUnit 5 + MockK

build.gradle.kts:

dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testImplementation("io.mockk:mockk:1.13.11")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
}

Разбор:

  • Вход: блок описывает тестовые зависимости в Gradle для модуля Kotlin.
  • Ключевые API: dependencies группирует библиотеки, testImplementation ограничивает их область тестами.
  • Поток выполнения: Gradle подтягивает эти артефакты при test, после чего JUnit/MockK/coroutines-test доступны в коде тестов.
  • Результат: проект умеет запускать JUnit 5, мокать зависимости и тестировать suspend-логику.
  • Типичная ошибка: забыть kotlinx-coroutines-test и получить неудобные/нестабильные тесты корутин.

Плагин JUnit 5 в tasks.test { useJUnitPlatform() } (в шаблонах IDEA часто уже есть).


Пример — сервис с suspend-репозиторием

Код ITЗагрузка примера кода…

Разбор:

  • interface UserRepo задаёт контракт доступа к данным; в тесте проверяется логика сервиса, а не БД.
  • suspend fun findName(id: Int) помечен как suspend, поэтому в тесте нужен coroutine-aware API.
  • class UserService(private val repo: UserRepo) — внедрение зависимости через конструктор упрощает подмену на мок.
  • mockk<UserRepo>() создаёт тестовую заглушку для интерфейса.
  • runTest { ... } запускает тест в контролируемой корутинной среде без реальных задержек.
  • coEvery { repo.findName(1) } returns "Ann" описывает поведение мока для suspend-вызова.
  • assertEquals("Hello, Ann!", service.greet(1)) проверяет конечный результат бизнес-логики.
  • coVerify { repo.findName(1) } подтверждает, что сервис действительно обратился к репозиторию.

Разбор построчно

СтрокаСмысл
interface UserRepoТестируем сервис; БД и сеть не нужны — подменим интерфейс.
mockk<UserRepo>()MockK создаёт объект-заглушку.
@Test fun greet() = runTest { }Тест — корутина: внутри можно вызывать suspend fun greet.
coEvery { repo.findName(1) } returns "Ann"При вызове findName(1) вернуть "Ann" без реальной БД.
service.greet(1)Проверяемый код.
assertEquals(...)Ожидаемая и фактическая строка совпали — тест зелёный.
coVerify { repo.findName(1) }Убедиться, что репозиторий действительно вызвали.

Важно: для suspend-методов используйте coEvery / coVerify. Обычный every { } даст ошибку вроде no answer found for suspend.

Запуск:

./gradlew test

Разбор:

  • ./gradlew запускает локальный Gradle Wrapper, поэтому версия Gradle берётся из проекта.
  • test — стандартная задача, которая компилирует и выполняет все тесты модуля.
  • Команда удобна для CI: тот же запуск работает в GitHub Actions и локально.

В IDEA: зелёная стрелка у класса UserServiceTest → Run.


Тест на ошибку

@Test
fun greetWhenRepoFails() = runTest {
coEvery { repo.findName(any()) } throws IOException("offline")
// проверьте, что сервис оборачивает ошибку или пробрасывает дальше
}

Разбор:

  • any() в MockK означает "любой аргумент", тест не привязан к конкретному id.
  • throws IOException("offline") имитирует отказ внешнего источника данных.
  • Такой тест фиксирует контракт обработки ошибок: преобразование в доменную ошибку или проброс исключения выше.
  • Негативные кейсы важны для стабильности: сервис проверяется не только на "счастливом пути".

Kotest

Kotest — тесты как спецификации на Kotlin:


import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

class CalculatorSpec : FunSpec({
test("add") {
Calculator().add(2, 3) shouldBe 5
}
})

Разбор:

  • FunSpec задаёт стиль тестов "как набор test-блоков" с хорошей читаемостью.
  • test("add") { ... } — именованный сценарий; имя видно в отчётах CI.
  • shouldBe — матчeр Kotest, читается как естественное сравнение expected и actual.
  • Конструкция полезна для спецификаций: легко группировать сценарии и расширять их по мере роста функционала.
СтильКогда
FunSpecПростые кейсы
DescribeSpec / BehaviorSpecBDD: given / when / then
forAllProperty-based — много случайных входов

На модуль лучше выбрать один стиль (JUnit или Kotest), чтобы команда не путалась.


Android — два вида тестов

Папка / зависимостьГде выполняется
src/test + testImplementationJVM на компьютере (ViewModel, парсеры)
src/androidTest + androidTestImplementationЭмулятор или устройство (UI, Room с Context)

Для корутин в ViewModel — runTest, StandardTestDispatcher, MainDispatcherRule — см. 231 Room + ViewModel.


Ktor — тест без внешнего сервера

Код ITЗагрузка примера кода…

Разбор:

  • testApplication { ... } поднимает тестовый Ktor-движок в памяти без реального открытого порта.
  • application { module() } подключает боевую конфигурацию приложения в тестовой среде.
  • client.get("/health") выполняет HTTP-запрос к in-memory приложению.
  • assertEquals(HttpStatusCode.OK, status) проверяет, что endpoint отвечает ожидаемым статусом.
  • Такой тест страхует от регрессий в routing и DI-конфигурации после рефакторинга.

Зависимость: testImplementation("io.ktor:ktor-server-test-host:$ktorVersion").

Короткая шпаргалка:

  • testApplication { } — поднимает движок Ktor в памяти теста.
  • application { module() } — та же настройка, что в продакшене.
  • client.get("/health") — как curl, но в коде.
  • status — HTTP-код (200, 404, …).

Удобно после Ktor — первая программа — первая программа Ktor: добавили маршрут — дописали тест.


Частые ошибки

СимптомПричина
no answer found for suspendНужен coEvery, не every
Тест виситНет runTest / не вызван advanceUntilIdle
MockK не мокает классКласс final — мокайте интерфейс
Ktor 404 в тестеНе подключён module() или другой routing

Что попробовать

  1. coVerify(exactly = 0) { repo.findName(2) } — убедиться, что лишний id не запрашивали.
  2. Kotest BehaviorSpec для одного use-case — сравнить читаемость с JUnit.
  3. Тест POST /notes с телом JSON в testApplication.

Дополнительный пример — проверка порядка вызовов

Код ITЗагрузка примера кода…

Разбор:

  • Вход: сервис делает три последовательных suspend-вызова к репозиторию.
  • Ключевые API: coEvery задаёт поведение моков, coVerifyOrder проверяет порядок вызовов.
  • Поток выполнения: тест запускает service.execute(), затем сверяет, что методы вызывались строго в ожидаемой последовательности.
  • Результат: тест ловит регрессии, где шаги случайно переставили местами.
  • Типичная ошибка: использовать coVerify вместо coVerifyOrder; тогда проверится только факт вызова, но не порядок.

Структура тестов в проекте

Когда тестов становится больше нескольких файлов, помогает простая и единая раскладка:

ПапкаЧто хранить
src/test/kotlin/.../unitunit-тесты без сети и БД
src/test/kotlin/.../integrationтесты с testApplication, Testcontainers, реальными адаптерами
src/test/resourcesJSON-фикстуры и конфиги для тестов

Для именования удобно правило Action_WhenCondition_ThenResult, например createNote_WhenBodyValid_Returns200.


Минимальный quality gate

Перед merge в main для Kotlin-сервиса обычно достаточно:

  • все unit-тесты зелёные;
  • критические endpoint'ы покрыты integration-тестами;
  • есть хотя бы один негативный сценарий на каждую важную операцию;
  • flaky-тесты устранены или изолированы.

Такой порог даёт стабильный CI даже в небольшой команде.


Дальше

Корутины · Ktor · Room · рекомендации


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.