Тестирование на 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 UI | Compose 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 / BehaviorSpec | BDD: given / when / then |
forAll | Property-based — много случайных входов |
На модуль лучше выбрать один стиль (JUnit или Kotest), чтобы команда не путалась.
Android — два вида тестов
| Папка / зависимость | Где выполняется |
|---|---|
src/test + testImplementation | JVM на компьютере (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 |
Что попробовать
coVerify(exactly = 0) { repo.findName(2) }— убедиться, что лишний id не запрашивали.- Kotest
BehaviorSpecдля одного use-case — сравнить читаемость с JUnit. - Тест
POST /notesс телом JSON вtestApplication.
Дополнительный пример — проверка порядка вызовов
Код ITЗагрузка примера кода…
Разбор:
- Вход: сервис делает три последовательных
suspend-вызова к репозиторию. - Ключевые API:
coEveryзадаёт поведение моков,coVerifyOrderпроверяет порядок вызовов. - Поток выполнения: тест запускает
service.execute(), затем сверяет, что методы вызывались строго в ожидаемой последовательности. - Результат: тест ловит регрессии, где шаги случайно переставили местами.
- Типичная ошибка: использовать
coVerifyвместоcoVerifyOrder; тогда проверится только факт вызова, но не порядок.
Структура тестов в проекте
Когда тестов становится больше нескольких файлов, помогает простая и единая раскладка:
| Папка | Что хранить |
|---|---|
src/test/kotlin/.../unit | unit-тесты без сети и БД |
src/test/kotlin/.../integration | тесты с testApplication, Testcontainers, реальными адаптерами |
src/test/resources | JSON-фикстуры и конфиги для тестов |
Для именования удобно правило Action_WhenCondition_ThenResult, например createNote_WhenBodyValid_Returns200.
Минимальный quality gate
Перед merge в main для Kotlin-сервиса обычно достаточно:
- все unit-тесты зелёные;
- критические endpoint'ы покрыты integration-тестами;
- есть хотя бы один негативный сценарий на каждую важную операцию;
- flaky-тесты устранены или изолированы.
Такой порог даёт стабильный CI даже в небольшой команде.
Дальше
Корутины · Ktor · Room · рекомендации
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.