Введение: зачем нужно интеграционное тестирование с Docker?
При разработке современных приложений на Kotlin, особенно микросервисов, важно проверять, как компоненты системы взаимодействуют друг с другом и с внешними зависимостями (базами данных, очередями сообщений, кэшами). Модульные тесты изолируют код, но не гарантируют, что приложение корректно работает в реальном окружении.
Интеграционное тестирование с Docker решает эту проблему. Docker позволяет поднимать изолированные контейнеры с необходимыми сервисами (PostgreSQL, Redis, Kafka) непосредственно перед запуском тестов и автоматически уничтожать их после завершения. Это даёт:
- Воспроизводимость: тесты всегда запускаются в одинаковом окружении.
- Изоляцию: контейнеры не влияют на локальную среду разработчика.
- Скорость: контейнеры запускаются за секунды, а не минуты.
В этой статье мы разберём, как настроить интеграционное тестирование Kotlin-приложения с использованием Docker, библиотеки Testcontainers и Docker Compose. Вы получите готовые примеры кода и лучшие практики.
1. Настройка проекта Kotlin для интеграционных тестов
1.1. Зависимости в build.gradle.kts
Для начала добавьте необходимые зависимости в ваш файл build.gradle.kts. Мы будем использовать JUnit 5, Testcontainers и драйвер для PostgreSQL:
dependencies {
// Основные зависимости приложения
implementation("org.jetbrains.kotlin:kotlin-stdlib")
implementation("org.postgresql:postgresql:42.7.1")
// Тестирование
testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
testImplementation("org.testcontainers:testcontainers:1.19.3")
testImplementation("org.testcontainers:postgresql:1.19.3")
testImplementation("org.testcontainers:junit-jupiter:1.19.3")
}
tasks.test {
useJUnitPlatform()
}
1.2. Базовая структура тестового класса
Создадим простой класс для интеграционного теста, который будет поднимать контейнер PostgreSQL:
import org.junit.jupiter.api.Test
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import java.sql.DriverManager
@Testcontainers
class UserRepositoryIntegrationTest {
companion object {
@Container
val postgres = PostgreSQLContainer<Nothing>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test")
}
@Test
fun `should connect to database and execute query`() {
val jdbcUrl = postgres.jdbcUrl
val username = postgres.username
val password = postgres.password
val connection = DriverManager.getConnection(jdbcUrl, username, password)
val statement = connection.createStatement()
statement.execute("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(100))")
statement.execute("INSERT INTO users (name) VALUES ('Alice')")
val resultSet = statement.executeQuery("SELECT COUNT(*) FROM users")
resultSet.next()
val count = resultSet.getInt(1)
assert(count == 1)
println("Тест пройден: найдено $count пользователей")
}
}
Обратите внимание: аннотация @Testcontainers автоматически управляет жизненным циклом контейнера. Он запускается перед всеми тестами в классе и останавливается после их завершения.
2. Тестирование с несколькими контейнерами (Docker Compose)
В реальных проектах часто требуется одновременно несколько сервисов: база данных, Redis, Kafka. Testcontainers поддерживает Docker Compose, что позволяет описать всю инфраструктуру в одном файле docker-compose.yml.
2.1. Файл docker-compose-test.yml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- "5432"
redis:
image: redis:7-alpine
ports:
- "6379"
2.2. Использование DockerComposeContainer в тесте
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 MultiServiceIntegrationTest {
companion object {
@Container
val environment = DockerComposeContainer<Nothing>(
File("src/test/resources/docker-compose-test.yml")
).withExposedService("postgres_1", 5432)
.withExposedService("redis_1", 6379)
}
@Test
fun `should connect to postgres and redis`() {
// Получаем динамический порт для PostgreSQL
val postgresPort = environment.getServicePort("postgres_1", 5432)
val redisPort = environment.getServicePort("redis_1", 6379)
println("PostgreSQL доступен на порту: $postgresPort")
println("Redis доступен на порту: $redisPort")
// Здесь можно подключаться к сервисам и выполнять тесты
assert(postgresPort > 0)
assert(redisPort > 0)
}
}
Testcontainers автоматически сопоставляет контейнеры с именами сервисов из Compose-файла. Вы можете получить динамические порты, чтобы избежать конфликтов.
3. Интеграционное тестирование с репозиториями и Spring Boot
Если вы используете Spring Boot, интеграционные тесты можно упростить с помощью аннотации @SpringBootTest и Testcontainers. Рассмотрим пример тестирования JPA-репозитория.
3.1. Конфигурация тестового профиля
Создайте файл application-test.yml:
spring:
datasource:
url: jdbc:tc:postgresql:15-alpine:///testdb
username: test
password: test
jpa:
hibernate:
ddl-auto: create-drop
Обратите внимание на URL: jdbc:tc:postgresql:.... Это специальный JDBC-URL от Testcontainers, который автоматически запускает контейнер при старте приложения.
3.2. Тест репозитория
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ActiveProfiles
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
@Entity
data class Product(
@Id @GeneratedValue(strategy = Generation