Интеграционное тестирование Kotlin с Docker: продвинутый уровень
Интеграционное тестирование — ключевой этап проверки взаимодействия компонентов приложения. В экосистеме Kotlin и Docker этот процесс выходит на новый уровень благодаря контейнеризации. В этой статье мы разберём продвинутые техники настройки и выполнения интеграционных тестов с использованием Docker, Testcontainers и Kotlin DSL.
1. Основы: Testcontainers с Kotlin DSL
Testcontainers — библиотека для Java/Kotlin, которая управляет жизненным циклом Docker-контейнеров внутри тестов. Для Kotlin доступен удобный DSL, упрощающий конфигурацию.
// build.gradle.kts
dependencies {
testImplementation("org.testcontainers:testcontainers:1.20.0")
testImplementation("org.testcontainers:postgresql:1.20.0")
testImplementation("org.jetbrains.kotlin:kotlin-test")
}
Пример теста с PostgreSQL:
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.junit.jupiter.api.Test
@Testcontainers
class UserRepositoryTest {
companion object {
@Container
val postgres = PostgreSQLContainer<Nothing>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test")
}
@Test
fun `should connect to database`() {
val jdbcUrl = postgres.jdbcUrl
// Используем jdbcUrl в DataSource
println("Connected to: $jdbcUrl")
}
}
2. Параллельный запуск контейнеров
Для ускорения тестов можно запускать контейнеры параллельно. Однако важно учитывать конфликты портов. Используйте динамические порты и синхронизацию через GenericContainer.
import org.testcontainers.containers.GenericContainer
import org.testcontainers.containers.Network
import org.testcontainers.containers.wait.strategy.Wait
import java.time.Duration
class ParallelContainerTest {
companion object {
private val network = Network.newNetwork()
private val redis1 = GenericContainer<Nothing>("redis:7-alpine")
.withNetwork(network)
.withNetworkAliases("redis1")
.withExposedPorts(6379)
.waitingFor(Wait.forListeningPort())
private val redis2 = GenericContainer<Nothing>("redis:7-alpine")
.withNetwork(network)
.withNetworkAliases("redis2")
.withExposedPorts(6379)
.waitingFor(Wait.forListeningPort())
init {
redis1.start()
redis2.start()
}
}
@Test
fun `parallel containers test`() {
val port1 = redis1.getMappedPort(6379)
val port2 = redis2.getMappedPort(6379)
println("Redis1 port: $port1, Redis2 port: $port2")
}
}
Используйте Network для изоляции и waitingFor для ожидания готовности контейнера.
3. Docker Compose в тестах
Для сложных сценариев (база данных + брокер сообщений + кэш) удобно использовать DockerComposeContainer. Он загружает docker-compose.yml и управляет всеми сервисами.
import org.testcontainers.containers.DockerComposeContainer
import java.io.File
class FullStackTest {
companion object {
private val compose = DockerComposeContainer<Nothing>(
File("src/test/resources/docker-compose.yml")
).apply {
withExposedService("postgres_1", 5432)
withExposedService("redis_1", 6379)
withLocalCompose(true)
start()
}
}
@Test
fun `full integration test`() {
val pgPort = compose.getServicePort("postgres_1", 5432)
val redisPort = compose.getServicePort("redis_1", 6379)
println("Postgres port: $pgPort, Redis port: $redisPort")
}
}
Пример docker-compose.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
4. Отладка и логирование контейнеров
При падении тестов важно видеть логи контейнеров. Используйте Slf4jLogConsumer или toString() для вывода.
import org.testcontainers.containers.output.Slf4jLogConsumer
import org.slf4j.LoggerFactory
class DebugContainerTest {
companion object {
private val logger = LoggerFactory.getLogger(DebugContainerTest::class.java)
private val container = GenericContainer<Nothing>("nginx:alpine")
.withExposedPorts(80)
.apply {
start()
followOutput(Slf4jLogConsumer(logger))
}
}
@Test
fun `debug test`() {
val logs = container.logs
println("Container logs: $logs")
}
}
Также полезно использовать withStartupTimeout(Duration.ofSeconds(60)) для предотвращения зависаний.
5. Продвинутые трюки: кастомные образы и скрипты
Иногда требуется запустить контейнер с предварительно выполненными скриптами. Используйте withCopyFileToContainer или кастомные Dockerfile.
import org.testcontainers.containers.BindMode
import org.testcontainers.images.builder.ImageFromDockerfile
class CustomImageTest {
companion object {
private val customImage = ImageFromDockerfile()
.withDockerfileFromBuilder { builder ->
builder
.from("openjdk:17-slim")
.copy("app.jar", "/app.jar")
.entryPoint("java", "-jar", "/app.jar")
}
private val container = GenericContainer<Nothing>(customImage)
.withExposedPorts(8080)
.withFileSystemBind("./build/libs/app.jar", "/app.jar", BindMode.READ_ONLY)
}
@Test
fun `custom image test`() {
container.start()
val port = container.getMappedPort(8080)
println("Custom app running on port: $port")
}
}
Заключение
Интеграционное тестирование Kotlin с Docker — мощный инструмент для проверки реального поведения приложения. Продвинутые техники, такие как параллельные контейнеры, Docker Compose, отладка и кастомные образы, позволяют создавать надёжные и быстрые тесты. Используйте Testcontainers с Kotlin DSL для автоматизации и изоляции окружения. Помните о динамических портах и правильном управлении ресурсами — это обеспечит стабильность ваших тестов.
Эти методы помогут вам перейти на новый уровень тестирования и избежать ти