Введение в тестирование Kotlin: от юнитов до end-to-end
Качественная кодовая база невозможна без автоматизированного тестирования. Kotlin, как современный язык для JVM, предоставляет разработчикам богатый инструментарий для создания надежных тестов. В этой статье мы разберем три уровня пирамиды тестирования применительно к Kotlin: unit (модульные), integration (интеграционные) и e2e (сквозные). Вы узнаете, как писать эффективные тесты на Kotlin с использованием популярных фреймворков, таких как JUnit 5, MockK, Ktor Client и Selenium.
1. Unit-тестирование в Kotlin: основа стабильности
Unit-тесты проверяют работу отдельных функций, классов или методов изолированно от внешних зависимостей. В Kotlin для этого чаще всего используется JUnit 5 в сочетании с библиотекой моков MockK.
1.1. Настройка проекта
Добавьте зависимости в файл build.gradle.kts:
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testImplementation("io.mockk:mockk:1.13.8")
}
1.2. Пример unit-теста с MockK
Предположим, у нас есть сервис UserService, который зависит от репозитория:
class UserService(private val repository: UserRepository) {
fun getUserName(id: Long): String {
val user = repository.findById(id)
return user?.name ?: "Unknown"
}
}
Тест с использованием MockK:
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class UserServiceTest {
@Test
fun `return userName when user exists`() {
val mockRepo = mockk<UserRepository>()
every { mockRepo.findById(1L) } returns User(1L, "Alice")
val service = UserService(mockRepo)
val result = service.getUserName(1L)
assertEquals("Alice", result)
}
}
1.3. Лучшие практики unit-тестирования
- Изолируйте зависимости — используйте моки для всех внешних вызовов (базы данных, API).
- Тестируйте граничные случаи: null, пустые строки, отрицательные числа.
- Используйте понятные имена тестов — они должны описывать сценарий и ожидаемый результат.
- Следуйте принципу AAA: Arrange (подготовка), Act (действие), Assert (проверка).
2. Интеграционное тестирование в Kotlin: проверка взаимодействия
Интеграционные тесты проверяют, как несколько компонентов системы работают вместе. В Kotlin это может быть тестирование REST API с использованием Ktor Client или проверка работы с базой данных через Exposed.
2.1. Тестирование REST API с Ktor
Создаем простой сервер на Ktor и тестируем его:
// Сервер
fun Application.module() {
routing {
get("/hello") {
call.respondText("Hello, Kotlin!")
}
}
}
// Тест
class ApiTest {
@Test
fun `test hello endpoint`() = testApplication {
application { module() }
val response = client.get("/hello")
assertEquals(HttpStatusCode.OK, response.status)
assertEquals("Hello, Kotlin!", response.bodyAsText())
}
}
2.2. Работа с тестовой базой данных
Для интеграционных тестов с БД используйте H2 (в памяти) или Testcontainers для PostgreSQL:
@Test
fun `test user creation`() {
withTestDatabase {
val user = User.new {
name = "Bob"
email = "bob@test.com"
}
assertEquals("Bob", user.name)
assertNotNull(user.id)
}
}
private fun withTestDatabase(block: () -> Unit) {
Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")
transaction {
SchemaUtils.create(User)
block()
}
}
2.3. Советы по интеграционным тестам
- Используйте in-memory базы для быстрых тестов, но учитывайте различия с продакшеном.
- Очищайте данные между тестами — используйте аннотацию
@BeforeEachили транзакционные коллбэки. - Тестируйте только один сценарий на тест-метод.
3. E2E-тестирование: проверка всей системы
End-to-end (E2E) тесты имитируют действия реального пользователя, проверяя работу приложения целиком — от интерфейса до базы данных. Для Kotlin-приложений часто используют Selenium WebDriver или Playwright.
3.1. Пример E2E теста с Selenium
class LoginE2ETest {
private lateinit var driver: WebDriver
@BeforeEach
fun setup() {
driver = ChromeDriver()
}
@Test
fun `user can login with valid credentials`() {
driver.get("http://localhost:8080/login")
driver.findElement(By.name("username")).sendKeys("admin")
driver.findElement(By.name("password")).sendKeys("password123")
driver.findElement(By.id("login-btn")).click()
val welcomeText = driver.findElement(By.id("welcome")).text
assertEquals("Welcome, admin!", welcomeText)
}
@AfterEach
fun teardown() {
driver.quit()
}
}
2.2. Запуск E2E тестов в CI/CD
Для автоматизации E2E тестов используйте:
- Testcontainers — для поднятия тестовых контейнеров (база данных, брокер сообщений).
- Gradle — настройте отдельную задачу для E2E:
tasks.register<Test>("e2eTest") {
useJUnitPlatform {
includeTags("e2e")
}
}
3.3. Важные принципы E2E
- Минимизируйте количество E2E тестов — они медленные и хрупкие.
- Тестируйте критически важные сценарии: регистрация, оформление заказа, вход.
- Используйте Page Object паттерн для организации к