Интеграционное тестирование 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);
// Используем полученные адреса для подключения