Тестирование в Kotlin: unit, integration, e2e от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Тестирование в Kotlin: unit, integration, e2e от А до Я

Современная разработка на Kotlin требует системного подхода к тестированию. Независимо от того, пишете ли вы backend на Ktor или мобильное приложение под Android, качественное покрытие кода тестами — залог стабильности и предсказуемости проекта. В этой статье мы разберём три уровня тестирования: модульные (unit), интеграционные (integration) и сквозные (e2e) тесты. Вы узнаете, какие инструменты использовать, как писать эффективные тесты на Kotlin и избегать типичных ошибок.

1. Unit-тестирование в Kotlin: основа основ

Unit-тесты проверяют работу отдельных функций, классов или методов в изоляции от внешних зависимостей. В Kotlin для этого чаще всего используют фреймворк JUnit 5 в сочетании с библиотекой моков MockK.

Настройка проекта

Добавьте зависимости в build.gradle.kts:

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
    testImplementation("io.mockk:mockk:1.13.8")
}

Пример unit-теста

Предположим, у нас есть простой сервис для расчёта скидки:

class DiscountService {
    fun applyDiscount(price: Double, discountPercent: Int): Double {
        require(discountPercent in 0..100) { "Процент скидки должен быть от 0 до 100" }
        return price * (100 - discountPercent) / 100
    }
}

Тест для этого сервиса:

import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows

class DiscountServiceTest {

    private val service = DiscountService()

    @Test
    fun `applyDiscount корректно считает скидку 50 процентов`() {
        val result = service.applyDiscount(1000.0, 50)
        assertEquals(500.0, result, 0.001)
    }

    @Test
    fun `applyDiscount выбрасывает исключение при неверном проценте`() {
        assertThrows<IllegalArgumentException> {
            service.applyDiscount(100.0, 150)
        }
    }
}

Совет: Используйте assertThrows для проверки исключений и именуйте тесты понятными строками — это улучшает читаемость отчётов.

Мокирование с MockK

Когда ваш класс зависит от других компонентов (например, репозитория или API), используйте моки:

class OrderService(private val paymentGateway: PaymentGateway) {
    fun processOrder(amount: Double): String {
        if (paymentGateway.charge(amount)) {
            return "Успешно оплачено"
        }
        return "Ошибка оплаты"
    }
}

// Тест
@Test
fun `processOrder возвращает успех при успешной оплате`() {
    val mockGateway = mockk<PaymentGateway>()
    every { mockGateway.charge(any()) } returns true

    val service = OrderService(mockGateway)
    val result = service.processOrder(500.0)

    assertEquals("Успешно оплачено", result)
    verify { mockGateway.charge(500.0) }
}

2. Интеграционное тестирование: проверка взаимодействия компонентов

Интеграционные тесты проверяют, как разные части системы работают вместе: база данных, внешние API, файловая система. Для Kotlin отлично подходит Testcontainers — библиотека для поднятия Docker-контейнеров во время тестов.

Пример с базой данных PostgreSQL

Добавьте зависимости:

testImplementation("org.testcontainers:testcontainers:1.19.3")
testImplementation("org.testcontainers:postgresql:1.19.3")

Напишем тест для DAO-слоя:

class UserRepositoryIntegrationTest {

    companion object {
        private val postgres = PostgreSQLContainer<Nothing>("postgres:15").apply {
            withDatabaseName("testdb")
            withUsername("test")
            withPassword("test")
            start()
        }
    }

    private val dataSource: DataSource = HikariDataSource(HikariConfig().apply {
        jdbcUrl = postgres.jdbcUrl
        username = postgres.username
        password = postgres.password
    })

    private val repository = UserRepository(dataSource)

    @Test
    fun `save и findById работают корректно`() {
        val user = User(id = null, name = "Иван", email = "ivan@example.com")
        val savedId = repository.save(user)

        val found = repository.findById(savedId)
        assertNotNull(found)
        assertEquals("Иван", found?.name)
    }
}

Важно: Используйте companion object и аннотацию @TestInstance(TestInstance.Lifecycle.PER_CLASS), чтобы контейнер запускался один раз для всех тестов класса.

3. E2E-тестирование: сквозная проверка пользовательских сценариев

End-to-end (E2E) тесты симулируют поведение реального пользователя. Для веб-приложений на Kotlin (например, на Ktor или Spring Boot) используйте Selenium или Playwright. Для Android — Espresso или Compose UI Test.

Пример E2E-теста для REST API

Протестируем полный цикл: создание пользователя через HTTP-запрос и проверку, что он появился в базе:

class UserE2ETest {

    private val client = HttpClient(CIO) {
        install(ContentNegotiation) { json() }
    }

    @Test
    fun `полный цикл создания и получения пользователя`(): Unit = runBlocking {
        // 1. Создаём пользователя
        val newUser = CreateUserRequest(name = "Пётр", email = "petr@test.com")
        val createResponse = client.post("http://localhost:8080/api/users") {
            setBody(newUser)
        }
        assertEquals(201, createResponse.status)

        // 2. Получаем всех пользователей
        val getResponse = client.get("http://localhost:8080/api/users")
        assertEquals(200, getResponse.status)

        val users: List<User> = getResponse.body()
        assertTrue(users.any { it.email == "petr@test.com" })
    }
}

Лучшая практика: Для E2E используйте выделенную тестовую среду (Docker Compose) и очищайте данные между запусками.

4. Лучшие практики и частые ошибки

  • Не тестируйте фреймворк. Проверяйте только свою бизнес-логику, а не работу JUnit или Ktor.
  • Изолируйте тесты. Каждый unit

Блоги

Рекомендаций по книгам