Тестирование в 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