Интеграционное тестирование Java с Docker: продвинутые техники

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

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

Начать курс

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

Интеграционное тестирование — ключевой этап разработки современных Java-приложений, особенно в микросервисной архитектуре. Использование Docker позволяет изолировать тестовые среды, гарантируя воспроизводимость и независимость от внешних сервисов. В этой статье мы рассмотрим продвинутые техники интеграционного тестирования Java-приложений с Docker, включая работу с библиотекой Testcontainers, настройку кастомных контейнеров, тестирование асинхронных сценариев и интеграцию в CI/CD пайплайны. Материал рассчитан на разработчиков, уже знакомых с основами JUnit и Docker.

Почему Docker критичен для интеграционного тестирования Java?

Традиционное интеграционное тестирование часто сталкивается с проблемами: нестабильные тестовые базы данных, конфликты портов, ручная настройка окружения. Docker решает эти проблемы, предоставляя легковесные, эфемерные контейнеры. Каждый тест может запускать собственную базу данных, брокер сообщений или кэш, а после завершения — автоматически их уничтожать. Это делает тесты:

  • Изолированными: каждый тестовый сценарий работает в чистом окружении.
  • Воспроизводимыми: одинаковое поведение на локальной машине и в CI.
  • Параллельными: контейнеры не конфликтуют по портам благодаря динамическому маппингу.

Для Java наиболее популярной библиотекой является Testcontainers, которая предоставляет простой API для управления контейнерами из тестов.

Настройка Testcontainers: от простого к сложному

Начнем с базового примера — тестирование DAO слоя с PostgreSQL. Добавьте зависимость в pom.xml:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.19.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.19.3</version>
    <scope>test</scope>
</dependency>

Теперь создадим тест, который использует контейнер 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.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import static org.junit.jupiter.api.Assertions.assertEquals;

@Testcontainers
public class UserRepositoryIntegrationTest {

    @Container
    private PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");

    @Test
    void shouldSaveAndFindUser() throws Exception {
        // Получаем динамический URL от контейнера
        String jdbcUrl = postgres.getJdbcUrl();
        String username = postgres.getUsername();
        String password = postgres.getPassword();

        try (Connection conn = DriverManager.getConnection(jdbcUrl, username, password)) {
            conn.createStatement().execute("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(255))");
            conn.createStatement().execute("INSERT INTO users (name) VALUES ('Alice')");
            ResultSet rs = conn.createStatement().executeQuery("SELECT name FROM users WHERE id = 1");
            rs.next();
            assertEquals("Alice", rs.getString("name"));
        }
    }
}

Testcontainers автоматически запускает контейнер перед тестами и останавливает после. Для продвинутого уровня используйте кастомные контейнеры (например, с предварительно загруженными данными) и сетевые изоляции:

// Кастомный контейнер с init скриптом
GenericContainer<?> customApp = new GenericContainer<>("my-app:latest")
        .withExposedPorts(8080)
        .withNetwork(Network.newNetwork())
        .withNetworkAliases("app");

Продвинутые сценарии: асинхронное тестирование и несколько контейнеров

Реальные приложения часто зависят от нескольких сервисов: базы данных, Redis, Kafka. Testcontainers поддерживает композиции. Рассмотрим пример с Kafka:

import org.testcontainers.containers.KafkaContainer;
import org.testcontainers.utility.DockerImageName;
import java.util.Properties;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;

@Container
private KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0"));

@Test
void shouldProduceAndConsumeMessage() {
    String bootstrapServers = kafka.getBootstrapServers();
    // Настройка продюсера и консюмера
    Properties props = new Properties();
    props.put("bootstrap.servers", bootstrapServers);
    // ... тестовая логика
}

Для асинхронных проверок (например, ожидание обработки события) используйте Awaitility в связке с Testcontainers:

import static org.awaitility.Awaitility.await;
import java.util.concurrent.TimeUnit;

@Test
void shouldProcessEventWithinTimeout() {
    // Запускаем асинхронную операцию
    eventService.sendEvent("test");
    // Ждем до 10 секунд, пока условие не станет true
    await().atMost(10, TimeUnit.SECONDS)
           .until(() -> eventService.getProcessedCount() == 1);
}

Это критично для тестирования микросервисов с event-driven архитектурой.

Интеграция в CI/CD: Docker Compose и динамические порты

В CI-среде (например, Jenkins, GitLab CI) важно, чтобы тесты не конфликтовали с другими сборками. Testcontainers автоматически использует случайные порты, но для сложных сценариев удобно использовать Docker Compose из тестов:

import org.testcontainers.containers.DockerComposeContainer;
import java.io.File;

@Container
public static DockerComposeContainer<?> environment =
    new DockerComposeContainer<>(new File("src/test/resources/docker-compose.yml"))
        .withExposedService("db_1", 5432)
        .withExposedService("redis_1", 6379);

@Test
void testWithCompose() {
    String dbHost = environment.getServiceHost("db_1", 5432);
    Integer dbPort = environment.getServicePort("db_1", 5432);
    // Используем полученные адреса для подключения                

Блоги

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