Интеграционное тестирование Kotlin с Docker: продвинутый уровень
Интеграционное тестирование — критически важный этап разработки современных микросервисов на Kotlin. Использование Docker позволяет создавать изолированные, воспроизводимые окружения, максимально приближенные к production. В этой статье мы рассмотрим продвинутые техники интеграционного тестирования Kotlin-приложений с Docker, включая Testcontainers, оптимизацию производительности и CI/CD-интеграцию.
1. Настройка Testcontainers для Kotlin-проекта
Testcontainers — это библиотека для JUnit, которая управляет жизненным циклом Docker-контейнеров прямо из тестов. Для Kotlin она предоставляет удобные DSL-расширения.
1.1. Подключение зависимостей
Добавьте в build.gradle.kts следующие зависимости:
testImplementation("org.testcontainers:testcontainers:1.19.3")
testImplementation("org.testcontainers:postgresql:1.19.3")
testImplementation("org.testcontainers:junit-jupiter:1.19.3")
testImplementation("org.jetbrains.kotlin:kotlin-test")
1.2. Базовый пример с PostgreSQL
Рассмотрим тест для репозитория, работающего с PostgreSQL:
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 UserRepositoryTest {
companion object {
@Container
val postgres = PostgreSQLContainer<Nothing>("postgres:15-alpine").apply {
withDatabaseName("testdb")
withUsername("test")
withPassword("test")
}
}
@Test
fun `should save and retrieve user`() {
val dataSource = createDataSource(postgres.jdbcUrl, postgres.username, postgres.password)
val repository = UserRepository(dataSource)
val user = User(id = null, name = "Alice", email = "alice@example.com")
val saved = repository.save(user)
val found = repository.findById(saved.id!!)
assert(found?.name == "Alice")
}
private fun createDataSource(jdbcUrl: String, username: String, password: String): DataSource {
// HikariCP или другой пул соединений
return HikariDataSource(HikariConfig().apply {
this.jdbcUrl = jdbcUrl
this.username = username
this.password = password
driverClassName = "org.postgresql.Driver"
})
}
}
Testcontainers автоматически запускает контейнер перед тестами и останавливает после завершения. Это гарантирует чистоту тестового окружения.
2. Тестирование нескольких сервисов с Docker Compose
В реальных микросервисах часто требуется взаимодействие с несколькими внешними системами: базами данных, очередями сообщений, кэшами. Используйте DockerComposeContainer для поднятия полного стека.
2.1. Конфигурация Docker Compose
Создайте файл docker-compose-test.yml:
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
redis:
image: redis:7-alpine
rabbitmq:
image: rabbitmq:3-management-alpine
2.2. Интеграционный тест с Docker Compose
Пример теста, проверяющего взаимодействие сервиса с БД, кэшем и очередью:
import org.junit.jupiter.api.Test
import org.testcontainers.containers.DockerComposeContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import java.io.File
@Testcontainers
class OrderServiceIntegrationTest {
companion object {
@Container
val environment = DockerComposeContainer(
File("src/test/resources/docker-compose-test.yml")
).apply {
withExposedService("postgres_1", 5432)
withExposedService("redis_1", 6379)
withExposedService("rabbitmq_1", 5672)
}
}
@Test
fun `should process order end-to-end`() {
val postgresUrl = environment.getServiceHost("postgres_1", 5432)
val redisUrl = environment.getServiceHost("redis_1", 6379)
val rabbitUrl = environment.getServiceHost("rabbitmq_1", 5672)
val orderService = OrderService(
database = DatabaseClient(postgresUrl),
cache = RedisClient(redisUrl),
queue = RabbitMQClient(rabbitUrl)
)
val order = Order("ORD-001", items = listOf("Book", "Pen"), total = 25.50)
val result = orderService.processOrder(order)
assert(result.isSuccess)
assert(result.orderStatus == "PROCESSED")
}
}
Важно: используйте withExposedService для доступа к контейнерам по динамическим портам, чтобы избежать конфликтов.
3. Оптимизация производительности интеграционных тестов
Интеграционные тесты с Docker могут быть медленными. Примените следующие техники для ускорения:
- Повторное использование контейнеров — используйте
@Testcontainers(parallel = true)и статические контейнеры для переиспользования между тестовыми классами. - Ленивая инициализация — запускайте контейнеры только перед первым тестом, а не перед каждым.
- Использование образов с предзагруженными данными — создайте кастомные Docker-образы с предварительно настроенными схемами и тестовыми данными.
- Параллельное выполнение — настройте JUnit на параллельный запуск тестовых классов.
3.1. Пример с переиспользованием контейнера
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
@Testcontainers
class SharedPostgresTest {
companion object {
@Container
val postgres = PostgreSQLContainer<Nothing>("postgres:15-alpine").apply {
withReuse(true) // Включаем переиспользование
}
}
@Test
fun `test 1`() { /* ... */ }
@Test
fun `test 2`() { /* ... */ }
}
Для работы withReuse(true) необходимо установить переменную окружения TESTCONTAINERS_REUSE_ENABLE=true. Контейнер будет остановлен только при завершении JVM-процесса.
4. Интеграция с CI/CD и обработка ошибок
При запуске тестов в CI-среде (GitHub Actions, GitLab CI) важны стабильность и логирование ошибок.
4.1. CI-конфигурация (GitHub Actions)
Пример workflow для запу