Интеграционное тестирование Kotlin с Docker: продвинутый уровень

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

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

Начать курс

Интеграционное тестирование 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 для запу

Блоги

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