Введение в тестирование в Kotlin: от юнит-тестов до end-to-end
Тестирование — неотъемлемая часть разработки надёжного программного обеспечения. В экосистеме Kotlin, который активно используется для Android-разработки, бэкенда на Ktor и многоплатформенных проектов, существует мощный инструментарий для всех уровней тестирования. В этой статье мы разберём три ключевых уровня: unit-тесты, интеграционные тесты и end-to-end (e2e) тесты. Вы узнаете, чем они отличаются, когда их применять и как писать эффективные тесты на Kotlin с примерами кода.
Unit-тестирование в Kotlin: основа надёжности
Unit-тесты проверяют работу отдельных компонентов (функций, классов) в изоляции. В Kotlin стандартом де-факто является фреймворк JUnit 5 в связке с библиотекой моков MockK.
Пример unit-теста с JUnit 5 и MockK
Рассмотрим простой сервис для расчёта скидки:
class DiscountService(private val userRepository: UserRepository) {
fun calculateDiscount(userId: String, amount: Double): Double {
val user = userRepository.findById(userId) ?: return amount
return if (user.isPremium) amount * 0.9 else amount
}
}
Тест для этого сервиса:
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class DiscountServiceTest {
@Test
fun `should apply 10% discount for premium user`() {
// Arrange
val userRepo = mockk<UserRepository>()
every { userRepo.findById("123") } returns User("123", isPremium = true)
val service = DiscountService(userRepo)
// Act
val result = service.calculateDiscount("123", 100.0)
// Assert
assertEquals(90.0, result)
}
@Test
fun `should not apply discount for regular user`() {
val userRepo = mockk<UserRepository>()
every { userRepo.findById("456") } returns User("456", isPremium = false)
val service = DiscountService(userRepo)
val result = service.calculateDiscount("456", 100.0)
assertEquals(100.0, result)
}
}
Ключевые моменты:
- Используйте
mockk{}для создания моков. - Блок
every { ... } returns ...задаёт поведение мока. - Структура Arrange-Act-Assert делает тесты читаемыми.
- Unit-тесты должны выполняться быстро и не зависеть от внешних ресурсов.
Интеграционное тестирование: проверка взаимодействия компонентов
Интеграционные тесты проверяют, как несколько модулей работают вместе — например, сервис с реальной базой данных или внешним API. Для Kotlin популярны Testcontainers (для Docker-контейнеров) и Spring Boot Test (для бэкенда).
Пример интеграционного теста с Testcontainers и Ktor
Предположим, у нас есть Ktor-приложение с PostgreSQL:
class UserRepository(private val database: Database) {
suspend fun findById(id: String): User? {
return database.query("SELECT * FROM users WHERE id = ?", id).singleOrNull { row ->
User(row.string("id"), row.boolean("is_premium"))
}
}
}
Интеграционный тест с Testcontainers:
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
@Testcontainers
class UserRepositoryIntegrationTest : StringSpec({
@Container
val postgres = PostgreSQLContainer<Nothing>("postgres:15").apply {
withDatabaseName("testdb")
withUsername("test")
withPassword("test")
}
"should find user by id" {
// Реальная инициализация подключения и создание таблицы
val database = createDatabase(postgres.jdbcUrl, postgres.username, postgres.password)
val repo = UserRepository(database)
val user = repo.findById("123")
user shouldBe User("123", true)
}
})
Рекомендации:
- Используйте Testcontainers для изоляции внешних зависимостей.
- Не тестируйте всю систему сразу — фокусируйтесь на границах модулей.
- Очищайте тестовые данные после каждого теста.
End-to-End (e2e) тестирование: проверка системы целиком
E2e-тесты симулируют поведение реального пользователя, проходя через все слои приложения: UI → API → база данных → внешние сервисы. Для Kotlin часто используют Selenium (веб), Espresso (Android) или Ktor Client для API.
Пример e2e-теста для REST API на Ktor
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable
import kotlin.test.Test
import kotlin.test.assertEquals
@Serializable
data class OrderResponse(val id: String, val status: String)
class OrderE2ETest {
private val client = HttpClient {
install(ContentNegotiation) { json() }
}
@Test
suspend fun `should create order successfully`() {
// 1. Регистрация пользователя
val token = registerUser(client)
// 2. Создание заказа
val response: OrderResponse = client.post("https://api.example.com/orders") {
header(HttpHeaders.Authorization, "Bearer $token")
setBody(mapOf("productId" to "prod-1", "quantity" to 2))
}.body()
// 3. Проверка статуса
assertEquals("pending", response.status)
// 4. Проверка через GET-запрос
val order: OrderResponse = client.get("https://api.example.com/orders/${response.id}") {
header(HttpHeaders.Authorization, "Bearer $token")
}.body()
assertEquals("pending", order.status)
}
}
Важные принципы e2e-тестов:
- Тестируйте критические пользовательские сценарии (логин, покупка, регистрация).
- Используйте отдельную тестовую среду (staging) — не трогайте продакшн.
- E2e-тесты медленные, поэтому их количество должно быть ограничено.
- Добавьте retry-