Testcontainers для Java: полное руководство с примерами

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

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

Начать курс

Testcontainers для Java: полное руководство с примерами

1. Что это и зачем нужна

Testcontainers — это мощная библиотека для Java (и других JVM-языков), которая позволяет запускать Docker-контейнеры прямо внутри ваших интеграционных тестов. Вместо того чтобы полагаться на внешние базы данных, брокеры сообщений или другие сервисы, которые могут быть недоступны или настроены по-разному в разных окружениях, Testcontainers автоматически поднимает необходимые контейнеры перед тестом и уничтожает их после завершения. Это гарантирует чистоту и воспроизводимость тестов.

Зачем это нужно? Традиционные интеграционные тесты часто страдают от "проблемы окружения": тест может пройти на машине разработчика, но упасть на CI-сервере из-за другой версии базы данных или отсутствия определённого сервиса. Testcontainers решает эту проблему раз и навсегда. Вы описываете необходимую инфраструктуру (например, "PostgreSQL 15 с включённым расширением uuid-ossp") прямо в коде теста, и библиотека гарантирует, что эта инфраструктура будет создана в изолированном Docker-контейнере. Это делает тесты по-настоящему портативными и надёжными.

Библиотека особенно полезна для тестирования DAO-слоёв, репозиториев, сервисов, работающих с очередями (RabbitMQ, Kafka), кэшами (Redis) и даже для тестирования HTTP-клиентов с помощью контейнеров с WireMock или Nginx. Testcontainers также предоставляет удобные модули для популярных технологий (PostgreSQL, MySQL, MongoDB, Redis, Kafka, LocalStack и многих других), что сводит настройку к минимуму.

2. Установка

Testcontainers распространяется через Maven Central. Для подключения к проекту на Java добавьте следующую зависимость в ваш pom.xml (Maven) или build.gradle (Gradle).

Maven (pom.xml)

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.19.7</version> <!-- Проверьте актуальную версию на Maven Central -->
    <scope>test</scope>
</dependency>

<!-- Дополнительные модули, например для PostgreSQL -->
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.19.7</version>
    <scope>test</scope>
</dependency>

Gradle (build.gradle)

testImplementation 'org.testcontainers:testcontainers:1.19.7'
testImplementation 'org.testcontainers:postgresql:1.19.7'

Важно: Убедитесь, что на вашей машине (или CI-агенте) установлен и запущен Docker Engine. Testcontainers взаимодействует с Docker через сокет. Если вы используете Docker Desktop на macOS или Windows, он должен быть запущен. На Linux убедитесь, что у пользователя, от которого запускаются тесты, есть права на работу с Docker (например, через группу docker).

3. Быстрый старт — минимальный рабочий пример

Давайте напишем простейший тест, который запускает контейнер с PostgreSQL, создаёт таблицу, вставляет данные и проверяет их. Это классический пример "Hello, World!" для 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 java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

import static org.junit.jupiter.api.Assertions.assertEquals;

@Testcontainers // Включает автоматическое управление жизненным циклом контейнеров
class QuickStartTest {

    // @Container указывает, что контейнером нужно управлять (запустить перед тестом, остановить после)
    @Container
    private PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15-alpine")
            .withDatabaseName("testdb")
            .withUsername("testuser")
            .withPassword("testpass");

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

        // 2. Подключаемся к базе данных внутри контейнера
        try (Connection conn = DriverManager.getConnection(jdbcUrl, username, password);
             Statement stmt = conn.createStatement()) {

            // 3. Создаём таблицу
            stmt.execute("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(255))");

            // 4. Вставляем данные
            stmt.execute("INSERT INTO users (name) VALUES ('Alice')");

            // 5. Читаем данные
            ResultSet rs = stmt.executeQuery("SELECT name FROM users WHERE id = 1");
            rs.next();
            String userName = rs.getString("name");

            // 6. Проверяем
            assertEquals("Alice", userName);
        }
    }
}

Как это работает? Аннотация @Testcontainers на уровне класса и @Container на поле контейнера заставляют JUnit запустить контейнер postgres:15-alpine перед выполнением тестов и остановить его после. Внутри теста мы получаем динамический JDBC URL, который указывает на наш только что запущенный контейнер, и работаем с ним как с обычной базой данных. Никаких внешних зависимостей — всё чисто и изолированно.

4. Основные методы/функции/классы

Рассмотрим ключевые классы и методы библиотеки. Все примеры предполагают использование JUnit 5 и аннотации @Testcontainers.

1. GenericContainer — универсальный контейнер

Сигнатура: public class GenericContainer<SELF extends GenericContainer<SELF>> extends FailureDetectingExternalResource implements Container<SELF>

Что делает: Базовый класс для запуска любого Docker-образа. Позволяет настроить порты, переменные окружения, команды и многое другое. Используется, когда для нужного сервиса нет специализированного модуля.

Пример:

@Container
static GenericContainer redis = new GenericContainer<>("redis:7-alpine")
        .withExposedPorts(6379);

@Test
void testRedisConnection() {
    String host = redis.getHost();
    Integer port = redis.getMappedPort(6379);
    // Теперь можно подключиться к Redis по host:port
    System.out.println("Redis доступен по адресу: " + host + ":" + port);
}

2. PostgreSQLContainer — специализированный контейнер для PostgreSQL

Сигнатура: public class PostgreSQLContainer<SELF extends PostgreSQLContainer<SELF>> extends JdbcDatabaseContainer<SELF>

Что делает: Предоставляет удобные методы для работы с PostgreSQL, такие как getJdbcUrl(), getUsername(), getPassword(), а также позволяет выполнять SQL-скрипты при старте.

Пример:

@Container
static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15")
        .withDatabaseName("integration_tests")
        .withUsername("sa")
        .withPassword("sa")
        .withInitScript("init.sql"); // Выполнит SQL из файла ресурсов

3. MongoDBContainer — контейнер для MongoDB

Сигнатура: public class MongoDBContainer extends GenericContainer<MongoDBContainer>

Что делает: Запускает MongoDB. Предоставляет метод getConnectionString() для получения строки подключения.

Пример:

@Container
static MongoDBContainer mongo = new MongoDBContainer("mongo:6");

@Test
void testMongoConnection() {
    String connectionString = mongo.getConnectionString();
    // Используйте connectionString для подключения MongoClient
    System.out.println("MongoDB URI: " + connectionString);
}

4. KafkaContainer — контейнер для Kafka

Сигнатура: public class KafkaContainer extends GenericContainer<KafkaContainer>

Что делает: Запускает Kafka (с встроенным ZooKeeper). Предоставляет метод getBootstrapServers().

Пример:

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

@Test
void testKafkaProducer() {
    String bootstrapServers = kafka.getBootstrapServers();
    // Настройте KafkaProducer с этими серверами
    System.out.println("Kafka bootstrap servers: " + bootstrapServers);
}

5. Network — создание пользовательской Docker-сети

Сигнатура: public class Network implements AutoCloseable

Что делает: Позволяет создавать изолированные Docker-сети для связи между контейнерами. Это полезно, когда один сервис (например, приложение) должен обращаться к другому (база данных) по имени контейнера, а не по IP.

Пример:

@Container
static Network network = Network.newNetwork();

@Container
static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15")
        .withNetwork(network)
        .withNetworkAliases("mydb"); // Контейнер будет доступен по имени "mydb"

@Container
static GenericContainer app = new GenericContainer<>("my-app:latest")
        .withNetwork(network)
        .withEnv("DB_URL", "jdbc:postgresql://mydb:5432/testdb"); // Ссылаемся на базу по алиасу

6. FixedHostPortGenericContainer — фиксация порта на хосте

Сигнатура: public class FixedHostPortGenericContainer<SELF extends FixedHostPortGenericContainer<SELF>> extends GenericContainer<SELF>

Что делает: Позволяет зафиксировать порт на хосте (вместо случайного). Используется с осторожностью, так как может привести к конфликтам портов при параллельном запуске тестов.

Пример:

@Container
static FixedHostPortGenericContainer webServer = 
        new FixedHostPortGenericContainer<>("nginx:alpine")
        .withFixedExposedPort(8080, 80); // Порт 80 контейнера будет доступен на хосте как 8080

7. Container.ExecResult — выполнение команд внутри контейнера

Сигнатура: public Container.ExecResult execInContainer(String... command) throws UnsupportedOperationException, IOException, InterruptedException

Что делает: Позволяет выполнить произвольную команду внутри запущенного контейнера и получить результат (stdout, stderr, код возврата). Полезно для проверки состояния или инициализации.

Пример:

@Container
static GenericContainer alpine = new GenericContainer<>("alpine:latest")
        .withCommand("sleep", "infinity"); // Держим контейнер запущенным

@Test
void testExecCommand() throws Exception {
    Container.ExecResult result = alpine.execInContainer("ls", "-la");
    System.out.println("stdout: " + result.getStdout());
    System.out.println("stderr: " + result.getStderr());
    assertEquals(0, result.getExitCode());
}

8. ImageFromDockerfile — создание образа из Dockerfile

Сигнатура: public class ImageFromDockerfile extends LazyDockerImageName

Что делает: Позволяет динамически создать Docker-образ из Dockerfile, который может быть передан как строка или как файл. Это полезно для тестирования собственных Docker-образов.

Пример:

@Container
static GenericContainer customApp = new GenericContainer<>(
        new ImageFromDockerfile("my-custom-app", false)
                .withDockerfileFromBuilder(builder -> builder
                        .from("openjdk:17-slim")
                        .copy("app.jar", "/app.jar")
                        .entryPoint("java", "-jar", "/app.jar"))
                .withFileFromClasspath("app.jar", "test-app.jar") // Файл из ресурсов теста
)
        .withExposedPorts(8080);

9. WaitStrategy — стратегии ожидания готовности контейнера

Сигнатура: public interface WaitStrategy (реализации: Wait.forHttp(...), Wait.forLogMessage(...), Wait.forListeningPort())

Что делает: Определяет, как Testcontainers будет ждать, пока контейнер не будет готов к работе. По умолчанию ждёт, пока откроется порт. Можно настроить ожидание HTTP-ответа или появления строки в логах.

Пример:

@Container
static GenericContainer nginx = new GenericContainer<>("nginx:alpine")
        .withExposedPorts(80)
        .waitingFor(Wait.forHttp("/") // Ждём HTTP 200 от корневого пути
                .forStatusCode(200)
                .withStartupTimeout(Duration.ofSeconds(30)));

10. @DynamicPropertySource — интеграция со Spring

Сигнатура: @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry)

Что делает: Позволяет динамически переопределить свойства Spring (например, URL базы данных) на основе запущенных контейнеров. Это ключевой метод для интеграции Testcontainers со Spring Boot тестами

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