Интеграционное тестирование Kotlin с Docker: полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Интеграционное тестирование 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                

Блоги

Рекомендаций по книгам