Почему E2E тестирование — ваш лучший друг (и как подружиться с ним в Kotlin)
Представьте: вы написали крутой сервис на Kotlin, всё скомпилировалось, модульные тесты зелёные, код-ревью прошло. Вы выкатываете в прод, и... пользователи жалуются, что кнопка «Купить» не работает, хотя юнит-тесты её проверяли. Знакомо? Добро пожаловать в мир, где E2E тестирование Kotlin приложений — не роскошь, а необходимость.
Сквозное (end-to-end) тестирование — это проверка всего пользовательского пути: от клика по кнопке до записи в базу данных и обратно. В этой статье мы не просто поговорим о теории. Мы напишем реальные тесты для Kotlin-приложения, разберём инструменты и покажем, как не сойти с ума при настройке инфраструктуры. Пристегнитесь, будет код и кофе.
Что такое E2E тестирование и чем оно отличается от других видов
Давайте сразу расставим точки над i. E2E (или сквозное) тестирование проверяет систему целиком, как её видит пользователь. В отличие от:
- Юнит-тестов — проверяют один метод/класс изолированно.
- Интеграционных тестов — проверяют взаимодействие двух-трёх компонентов (например, контроллер + сервис).
- UI-тестов — часто путают с E2E, но UI-тест может не проверять бэкенд, а только отрисовку интерфейса.
E2E тестирование Kotlin приложений — это полный цикл: пользовательский интерфейс (если есть), API, бизнес-логика, база данных, внешние сервисы. Всё должно работать вместе как часы.
Инструменты для E2E тестирования в Kotlin
Выбор инструмента зависит от того, что вы тестируете: Android-приложение, веб-сервис на Ktor или десктопное приложение. Вот топ для Kotlin-разработчика:
- Selenide + Kotlin — для веб-интерфейсов. Обёртка над Selenium, но с лаконичным синтаксисом, идеально подходит для Kotlin.
- Ktor Client + TestContainers — для бэкенда. Поднимаете тестовую инфраструктуру (БД, кеш) в Docker и гоняете HTTP-запросы.
- Espresso + Compose Test — для Android. Нативные инструменты, которые поставляются с Android SDK.
- Cucumber + Kotlin — если нужны BDD-сценарии на понятном языке для заказчика.
Мы сосредоточимся на двух первых сценариях, так как они наиболее универсальны.
Пишем E2E тест для веб-приложения на Kotlin (Selenide)
Допустим, у нас есть простой интернет-магазин на Spring Boot + Kotlin. Нам нужно проверить сценарий: пользователь заходит на сайт, добавляет товар в корзину и оформляет заказ.
Сначала добавим зависимости в build.gradle.kts:
dependencies {
testImplementation("com.codeborne:selenide:6.19.1")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testImplementation("io.github.bonigarcia:webdrivermanager:5.6.2")
}
Теперь напишем сам тест. Обратите внимание: мы используем extension-функции Kotlin, чтобы сделать код читаемым:
import com.codeborne.selenide.Condition.visible
import com.codeborne.selenide.Selenide.`$`
import com.codeborne.selenide.Selenide.open
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class ShopE2ETest {
@BeforeEach
fun setUp() {
// Открываем браузер перед каждым тестом
open("https://test-shop.example.com")
}
@Test
fun `user can add product to cart and checkout`() {
// Шаг 1: Найти товар и добавить в корзину
`$`("[data-testid='product-1']").shouldBe(visible).click()
`$`("[data-testid='add-to-cart']").click()
// Шаг 2: Проверить, что корзина не пуста
`$`("[data-testid='cart-badge']").shouldHave(text("1"))
// Шаг 3: Перейти к оформлению
`$`("[data-testid='checkout-btn']").click()
// Шаг 4: Заполнить форму
`$`("[name='email']").setValue("test@example.com")
`$`("[name='address']").setValue("ул. Программистов, д. 42")
`$`("[data-testid='submit-order']").click()
// Шаг 5: Проверить успешное оформление
`$`("[data-testid='order-success']").shouldBe(visible)
}
}
Важно: Используйте data-testid атрибуты, а не CSS-классы или XPath. Это делает тесты устойчивыми к изменениям вёрстки.
E2E тестирование бэкенда на Ktor с TestContainers
Теперь представим, что у нас микросервис на Ktor, который общается с PostgreSQL. Нам нужно проверить полный цикл: POST запрос → сохранение в БД → GET запрос возвращает данные.
Добавим зависимости:
dependencies {
testImplementation("io.ktor:ktor-server-test-host:2.3.7")
testImplementation("org.testcontainers:postgresql:1.19.3")
testImplementation("org.jetbrains.exposed:exposed:0.44.1")
}
Вот как выглядит E2E тест для такого сценария:
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*
import org.junit.jupiter.api.Test
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
@Testcontainers
class UserServiceE2ETest {
companion object {
@Container
val postgres = PostgreSQLContainer<Nothing>("postgres:15").apply {
withDatabaseName("testdb")
withUsername("test")
withPassword("test")
}
}
@Test
fun `create user and retrieve it via API`() = testApplication {
// Настраиваем приложение с тестовой БД
environment {
config = MapApplicationConfig(
"db.url" to postgres.jdbcUrl,
"db.user" to postgres.username,
"db.password" to postgres.password
)
}
application {
module() // ваш модуль приложения
}
// Шаг 1: Создаём пользователя
val createResponse = client.post("/api/users") {
contentType(ContentType.Application.Json)
setBody("""{"name": "Иван Петров", "email": "ivan@test.com"}""")
}
assert(createResponse.status == HttpStatusCode.Created)
// Шаг 2: Получаем список пользователей
val getResponse = client.get("/api/users")
val body = getResponse.bodyAsText()
// Шаг 3: Проверяем, что наш пользователь там есть
assert(body.contains("Иван Петров"))
assert(body.contains("ivan@test.com"))
}
}
Обратите внимание: TestCont