Integration Testing Java with Docker: From Basics to Advanced Techniques
Integration testing is a critically important stage in the development of modern Java applications, especially in a microservices architecture. Docker has become the de facto standard for isolating test environments, but simply running a container is not enough. At an advanced level, container lifecycle management, dynamic network configuration, parallel test execution, and seamless CI/CD integration are required. In this article, we will cover complex scenarios of integration testing Java with Docker using the Testcontainers library and show how to achieve maximum performance and reliability.
1. Dynamic Container Management with Testcontainers
Testcontainers is a Java library that allows you to programmatically create and destroy Docker containers in JUnit tests. At an advanced level, we use not only standard modules (PostgreSQL, Kafka) but also custom containers.
Example: Custom container for Redis with clustering
import org.testcontainers.containers.GenericContainer;import org.testcontainers.containers.wait.strategy.Wait;import org.testcontainers.junit.jupiter.Container;import org.testcontainers.junit.jupiter.Testcontainers;import org.junit.jupiter.api.Test;
import java.time.Duration;
@Testcontainerspublic class RedisClusterTest {
@Container private static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine") .withExposedPorts(6379) .withCommand("redis-server", "--cluster-enabled", "yes") .waitingFor(Wait.forLogMessage(".*Ready to accept connections.*\\", 1) .withStartupTimeout(Duration.ofSeconds(30)));
@Test void testRedisConnection() { String host = redis.getHost(); Integer port = redis.getMappedPort(6379); System.out.println("Redis available at: " + host + ":" + port); // Code for connecting to Redis and performing operations goes here }}Key advanced features:
- Waiting strategies — instead of fixed timeouts, we use waiting for logs, ports, or HTTP responses.
- Dynamic ports — avoid port conflicts during parallel execution.
- Custom commands — run containers with non-standard configuration (e.g., Redis in cluster mode).
2. Complex Network Topologies and Docker Compose in Tests
For testing microservices, it is often necessary to spin up several interconnected containers (DB, cache, message broker). Using Docker Compose inside tests is an advanced approach that allows describing the entire infrastructure in YAML.
Example: Integration with Kafka and PostgreSQL via Docker Compose
import org.testcontainers.containers.DockerComposeContainer;import org.testcontainers.junit.jupiter.Container;import org.testcontainers.junit.jupiter.Testcontainers;import org.junit.jupiter.api.Test;
import java.io.File;
@Testcontainerspublic class MicroserviceIntegrationTest {
@Container private static DockerComposeContainer<?> environment = new DockerComposeContainer<>(new File("src/test/resources/docker-compose.yml")) .withExposedService("postgres_1", 5432) .withExposedService("kafka_1", 9092);
@Test void testServiceInteraction() { String pgHost = environment.getServiceHost("postgres_1", 5432); Integer pgPort = environment.getServicePort("postgres_1", 5432); String kafkaHost = environment.getServiceHost("kafka_1", 9092); Integer kafkaPort = environment.getServicePort("kafka_1", 9092); // Initialize clients and execute