Интеграционное тестирование Kotlin с Docker: от А до Я
В современной разработке на Kotlin интеграционное тестирование с использованием Docker стало стандартом де-факто. Контейнеризация позволяет создавать изолированные, воспроизводимые тестовые окружения, которые максимально приближены к production-среде. В этой статье мы разберем полный процесс настройки и написания интеграционных тестов для Kotlin-приложений с помощью Docker и библиотеки Testcontainers.
Почему Docker важен для интеграционного тестирования Kotlin?
Традиционные интеграционные тесты часто страдают от проблем с окружением: разные версии баз данных, несовместимость драйверов, "плавающие" тесты из-за фоновых процессов. Docker решает эти проблемы, предоставляя:
- Изоляцию: каждый тест запускается в чистом контейнере
- Воспроизводимость: одинаковое окружение на любой машине
- Масштабируемость: возможность тестировать сложные микросервисные архитектуры
- Скорость: быстрый запуск и остановка контейнеров
Настройка проекта Kotlin для интеграционных тестов с Docker
Для начала добавим необходимые зависимости в build.gradle.kts:
dependencies {
// Testcontainers
testImplementation("org.testcontainers:testcontainers:1.19.3")
testImplementation("org.testcontainers:postgresql:1.19.3")
testImplementation("org.testcontainers:kafka:1.19.3")
testImplementation("org.testcontainers:junit-jupiter:1.19.3")
// База данных
testImplementation("org.postgresql:postgresql:42.7.1")
// JUnit 5
testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
// Для Spring Boot приложений
testImplementation("org.springframework.boot:spring-boot-starter-test:3.2.1")
testImplementation("org.springframework.boot:spring-boot-testcontainers:3.2.1")
}
Базовый пример: тестирование с PostgreSQL
Рассмотрим простейший пример - тестирование репозитория, работающего с PostgreSQL. Создадим интеграционный тест с использованием Testcontainers:
import org.junit.jupiter.api.Test
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import javax.sql.DataSource
@Testcontainers
class UserRepositoryIntegrationTest {
companion object {
@Container
val postgres = PostgreSQLContainer<Nothing>("postgres:15-alpine").apply {
withDatabaseName("testdb")
withUsername("test")
withPassword("test")
}
}
@Test
fun `should save and find user`() {
val dataSource = createDataSource(postgres)
val repository = UserRepository(dataSource)
val user = User(id = null, name = "Иван Петров", email = "ivan@example.com")
val savedUser = repository.save(user)
val foundUser = repository.findById(savedUser.id!!)
assert(foundUser != null)
assert(foundUser?.name == "Иван Петров")
}
private fun createDataSource(container: PostgreSQLContainer<*>): DataSource {
val config = HikariConfig().apply {
jdbcUrl = container.jdbcUrl
username = container.username
password = container.password
driverClassName = container.driverClassName
}
return HikariDataSource(config)
}
}
Тестирование микросервисов с Kafka и Docker Compose
Для более сложных сценариев, когда требуется несколько сервисов, используйте Docker Compose через Testcontainers:
import org.testcontainers.containers.DockerComposeContainer
import org.testcontainers.containers.wait.strategy.Wait
import java.io.File
class OrderServiceIntegrationTest {
companion object {
private val compose = DockerComposeContainer<Nothing>(
File("src/test/resources/docker-compose-test.yml")
).apply {
withExposedService("postgres_1", 5432)
withExposedService("kafka_1", 9092)
withExposedService("redis_1", 6379)
waitingFor("postgres_1", Wait.forHealthcheck())
waitingFor("kafka_1", Wait.forListeningPort())
}
init {
compose.start()
}
}
@Test
fun `test order creation with event publishing`() {
val kafkaContainer = compose.getContainerByServiceName("kafka_1").get()
val postgresContainer = compose.getContainerByServiceName("postgres_1").get()
// Настройка подключений
val kafkaProperties = KafkaProperties(
bootstrapServers = "localhost:${kafkaContainer.getMappedPort(9092)}"
)
val orderService = OrderService(
orderRepository = createOrderRepository(postgresContainer),
eventPublisher = KafkaEventPublisher(kafkaProperties)
)
// Выполнение теста
val order = Order(userId = 1, items = listOf("item-1", "item-2"))
val result = orderService.createOrder(order)
assert(result.isSuccess)
assert(result.getOrNull()?.status == OrderStatus.CREATED)
}
}
Продвинутые техники: динамическая конфигурация и параллельные тесты
Динамическое управление контейнерами
Для ускорения тестов используйте shared контейнеры и динамическую настройку:
import org.testcontainers.containers.GenericContainer
import org.testcontainers.containers.Network
import org.testcontainers.containers.wait.strategy.Wait
class DynamicContainerTest {
private val network = Network.newNetwork()
@Test
fun `test with dynamically configured containers`() {
val redis = GenericContainer<Nothing>("redis:7-alpine").apply {
withNetwork(network)
withNetworkAliases("redis")
withExposedPorts(6379)
waitingFor(Wait.forLogMessage(".*Ready to accept connections.*", 1))
start()
}
val app = GenericContainer<Nothing>("my-app:latest").apply {
withNetwork(network)
withEnv("REDIS_HOST", "redis")
withEnv("REDIS_PORT", "6379")
withExposedPorts(8080)
dependsOn(redis)
waitingFor(Wait.forHttp("/health").forStatusCode(200))
start()
}
// Тестирование через HTTP-клиент
val response = khttp.get("http://localhost:${app.firstMappedPort}/api/users")
assert(response.statusCode == 200)
}
}
Параллельное выполнение тестов
Настройте JUnit для параллельного запуска тестов с разными контейнерами:
// junit-platform.properties
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = concurrent
// Тестовый класс
@TestInstance(TestInstance.Lifecycle.PER