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 тестами