Using Docker in JUnit tests enables you to test your application against real dependencies like databases, message brokers, and external services in isolated containers. This provides more realistic testing than mocks or embedded services.
Why Docker in JUnit Tests?
Advantages:
- Real Services: Test against actual databases, message brokers, caches
- Isolation: Clean environment for each test
- Consistency: Same environment in development and CI
- Complex Scenarios: Test multi-service interactions
- No Mocks: Real network calls and service behavior
- Easy Cleanup: Containers are automatically removed
Setup and Dependencies
Maven Dependencies
<properties>
<testcontainers.version>1.19.3</testcontainers.version>
</properties>
<dependencies>
<!-- Testcontainers Core -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<!-- JUnit 5 Integration -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<!-- Database Modules -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<!-- Other Services -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>kafka</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>redis</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Basic Docker Container Testing
Example 1: Simple Database Container Test
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.*;
@Testcontainers
class BasicDockerTest {
// Start PostgreSQL container before any tests
@Container
private static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Test
void testPostgreSQLConnection() throws Exception {
// Get container connection details
String jdbcUrl = postgres.getJdbcUrl();
String username = postgres.getUsername();
String password = postgres.getPassword();
// Test database connection and operations
try (Connection conn = DriverManager.getConnection(jdbcUrl, username, password);
Statement stmt = conn.createStatement()) {
// Create table
stmt.execute("CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(100), email VARCHAR(100))");
// Insert data
stmt.execute("INSERT INTO users (name, email) VALUES ('John Doe', '[email protected]')");
// Query data
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
// Verify results
assertTrue(rs.next());
assertEquals("John Doe", rs.getString("name"));
assertEquals("[email protected]", rs.getString("email"));
}
}
@Test
void testDatabaseIsolation() throws Exception {
// Each test gets a clean database (if not using reuse)
String jdbcUrl = postgres.getJdbcUrl();
try (Connection conn = DriverManager.getConnection(jdbcUrl, postgres.getUsername(), postgres.getPassword());
Statement stmt = conn.createStatement()) {
// Verify the table from previous test doesn't exist
ResultSet rs = stmt.executeQuery(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'users'");
rs.next();
assertEquals(0, rs.getInt(1));
}
}
}
Example 2: Multiple Service Containers
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import redis.clients.jedis.Jedis;
import static org.junit.jupiter.api.Assertions.*;
@Testcontainers
class MultipleContainersTest {
@Container
private static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb");
@Container
private static final GenericContainer<?> redis =
new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379)
.waitingFor(Wait.forLogMessage(".*Ready to accept connections.*\\n", 1));
@Test
void testPostgreSQL() throws Exception {
String jdbcUrl = postgres.getJdbcUrl();
try (var conn = DriverManager.getConnection(jdbcUrl, postgres.getUsername(), postgres.getPassword());
var stmt = conn.createStatement()) {
stmt.execute("SELECT 1");
assertTrue(conn.isValid(2)); // 2 second timeout
}
}
@Test
void testRedis() {
// Get mapped Redis port
Integer redisPort = redis.getMappedPort(6379);
String redisHost = redis.getHost();
try (Jedis jedis = new Jedis(redisHost, redisPort)) {
jedis.set("test-key", "test-value");
String value = jedis.get("test-key");
assertEquals("test-value", value);
}
}
@Test
void testBothServices() throws Exception {
// Test interaction between services
testPostgreSQL();
testRedis();
}
}
Spring Boot Integration
Example 3: Spring Boot with Docker Containers
Application Configuration:
// src/main/java/com/example/config/AppConfig.java
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
// Configure in application.properties
return DataSourceBuilder.create().build();
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName("localhost");
config.setPort(6379);
return new LettuceConnectionFactory(config);
}
}
Test Configuration:
// src/test/java/com/example/config/TestContainersConfig.java
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
@TestConfiguration
public class TestContainersConfig {
@Bean
@ServiceConnection
public PostgreSQLContainer<?> postgreSQLContainer() {
return new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
}
@Bean
public GenericContainer<?> redisContainer() {
return new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
}
}
Service Tests:
// src/test/java/com/example/service/UserServiceTest.java
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Testcontainers
class UserServiceTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine");
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Test
void shouldCreateUser() {
User user = new User("John Doe", "[email protected]");
User saved = userService.createUser(user);
assertNotNull(saved.getId());
assertEquals("John Doe", saved.getName());
assertEquals("[email protected]", saved.getEmail());
}
@Test
void shouldFindUserByEmail() {
User user = new User("Jane Smith", "[email protected]");
userService.createUser(user);
Optional<User> found = userService.findByEmail("[email protected]");
assertTrue(found.isPresent());
assertEquals("Jane Smith", found.get().getName());
}
}
Advanced Docker Testing Scenarios
Example 4: Kafka Testing with Testcontainers
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.KafkaContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import java.time.Duration;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.*;
@Testcontainers
class KafkaTest {
@Container
private static final KafkaContainer kafka =
new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0"));
@Test
void testKafkaProducerConsumer() throws Exception {
String bootstrapServers = kafka.getBootstrapServers();
// Configure producer
Properties producerProps = new Properties();
producerProps.put("bootstrap.servers", bootstrapServers);
producerProps.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
producerProps.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// Configure consumer
Properties consumerProps = new Properties();
consumerProps.put("bootstrap.servers", bootstrapServers);
consumerProps.put("group.id", "test-group");
consumerProps.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
consumerProps.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
consumerProps.put("auto.offset.reset", "earliest");
// Test Kafka operations
try (KafkaProducer<String, String> producer = new KafkaProducer<>(producerProps);
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps)) {
String topic = "test-topic";
consumer.subscribe(Collections.singletonList(topic));
// Send message
ProducerRecord<String, String> record =
new ProducerRecord<>(topic, "key", "Hello Kafka!");
producer.send(record).get(5, TimeUnit.SECONDS);
// Receive message
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(10));
assertEquals(1, records.count());
ConsumerRecord<String, String> received = records.iterator().next();
assertEquals("key", received.key());
assertEquals("Hello Kafka!", received.value());
}
}
}
Example 5: Custom Docker Image Testing
Dockerfile for Test Service:
FROM openjdk:17-jre-slim COPY target/test-service.jar /app/test-service.jar EXPOSE 8080 CMD ["java", "-jar", "/app/test-service.jar"]
Custom Container Test:
import org.junit.jupiter.api.Test;
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.testcontainers.images.builder.ImageFromDockerfile;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import static org.junit.jupiter.api.Assertions.*;
@Testcontainers
class CustomContainerTest {
@Container
private static final GenericContainer<?> customService =
new GenericContainer<>(
new ImageFromDockerfile()
.withDockerfileFromBuilder(builder ->
builder
.from("openjdk:17-jre-slim")
.copy("test-service.jar", "/app/")
.expose(8080)
.cmd("java", "-jar", "/app/test-service.jar")
.build()
)
.withFileFromFile("test-service.jar",
new File("target/test-service.jar"))
)
.withExposedPorts(8080)
.waitingFor(Wait.forHttp("/health").forStatusCode(200));
@Test
void testCustomService() throws Exception {
String baseUrl = String.format("http://%s:%d",
customService.getHost(),
customService.getMappedPort(8080));
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/health"))
.GET()
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode());
assertTrue(response.body().contains("OK"));
}
}
Example 6: Network Testing with Multiple Containers
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
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 static org.junit.jupiter.api.Assertions.*;
@Testcontainers
class NetworkTest {
private static final Network network = Network.newNetwork();
@Container
private static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withNetwork(network)
.withNetworkAliases("postgres");
@Container
private static final GenericContainer<?> app =
new GenericContainer<>("my-app:latest")
.withNetwork(network)
.withEnv("DB_URL", "jdbc:postgresql://postgres:5432/testdb")
.withEnv("DB_USERNAME", "test")
.withEnv("DB_PASSWORD", "test")
.withExposedPorts(8080)
.dependsOn(postgres);
@Test
void testContainersCanCommunicate() throws Exception {
// Test database connection from app container
String result = app.execInContainer(
"curl", "-s", "http://localhost:8080/health")
.getStdout();
assertTrue(result.contains("OK"));
// Test direct database access
String jdbcUrl = postgres.getJdbcUrl();
try (Connection conn = DriverManager.getConnection(jdbcUrl,
postgres.getUsername(), postgres.getPassword())) {
assertTrue(conn.isValid(2));
}
}
}
Performance Optimization
Example 7: Container Reuse and Parallel Execution
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.junit.jupiter.api.Assertions.*;
@Testcontainers
@Execution(ExecutionMode.CONCURRENT) // Run tests in parallel
class ParallelContainerTest {
// Reuse container across test methods
@Container
private static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withReuse(true); // Enable container reuse
@Test
void testOne() throws Exception {
executeTest("test_one");
}
@Test
void testTwo() throws Exception {
executeTest("test_two");
}
@Test
void testThree() throws Exception {
executeTest("test_three");
}
private void executeTest(String tableName) throws Exception {
String jdbcUrl = postgres.getJdbcUrl();
try (var conn = DriverManager.getConnection(jdbcUrl,
postgres.getUsername(), postgres.getPassword());
var stmt = conn.createStatement()) {
// Each test creates its own table
stmt.execute(String.format(
"CREATE TABLE %s (id SERIAL PRIMARY KEY, data VARCHAR(100))",
tableName));
stmt.execute(String.format(
"INSERT INTO %s (data) VALUES ('%s')",
tableName, tableName));
var rs = stmt.executeQuery(
String.format("SELECT data FROM %s", tableName));
rs.next();
assertEquals(tableName, rs.getString("data"));
}
}
}
Best Practices
Example 8: Test Lifecycle Management
import org.junit.jupiter.api.*;
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.Statement;
import static org.junit.jupiter.api.Assertions.*;
@Testcontainers
class LifecycleTest {
@Container
private static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb");
@BeforeAll
static void setUpClass() {
System.out.println("PostgreSQL container started: " +
postgres.getJdbcUrl());
}
@BeforeEach
void setUp() throws Exception {
// Clean database before each test
try (Connection conn = DriverManager.getConnection(
postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword());
Statement stmt = conn.createStatement()) {
stmt.execute("DROP TABLE IF EXISTS users");
stmt.execute("CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(100))");
}
}
@Test
void testUserCreation() throws Exception {
try (Connection conn = DriverManager.getConnection(
postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword());
Statement stmt = conn.createStatement()) {
stmt.execute("INSERT INTO users (name) VALUES ('Test User')");
var rs = stmt.executeQuery("SELECT COUNT(*) FROM users");
rs.next();
assertEquals(1, rs.getInt(1));
}
}
@AfterEach
void tearDown() throws Exception {
// Optional cleanup after each test
try (Connection conn = DriverManager.getConnection(
postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword());
Statement stmt = conn.createStatement()) {
stmt.execute("DROP TABLE IF EXISTS users");
}
}
@AfterAll
static void tearDownClass() {
System.out.println("Tests completed. Container will be stopped.");
}
}
Common Patterns and Solutions
Example 9: Shared Container Configuration
// Shared container configuration for multiple test classes
public class TestContainersFactory {
private static final PostgreSQLContainer<?> POSTGRES_CONTAINER;
static {
POSTGRES_CONTAINER = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test")
.withReuse(true);
if (!POSTGRES_CONTAINER.isRunning()) {
POSTGRES_CONTAINER.start();
}
// Register shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(POSTGRES_CONTAINER::stop));
}
public static PostgreSQLContainer<?> getPostgreSQLContainer() {
return POSTGRES_CONTAINER;
}
public static String getJdbcUrl() {
return POSTGRES_CONTAINER.getJdbcUrl();
}
public static String getUsername() {
return POSTGRES_CONTAINER.getUsername();
}
public static String getPassword() {
return POSTGRES_CONTAINER.getPassword();
}
}
// Test class using shared container
@Testcontainers
class SharedContainerTest {
@Test
void testWithSharedContainer() throws Exception {
String jdbcUrl = TestContainersFactory.getJdbcUrl();
String username = TestContainersFactory.getUsername();
String password = TestContainersFactory.getPassword();
try (Connection conn = DriverManager.getConnection(jdbcUrl, username, password);
Statement stmt = conn.createStatement()) {
stmt.execute("SELECT 1");
assertTrue(conn.isValid(2));
}
}
}
Troubleshooting
Docker Daemon Issues
import org.junit.jupiter.api.BeforeAll;
import org.testcontainers.DockerClientFactory;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
class DockerCheckTest {
@BeforeAll
static void checkDocker() {
assumeTrue(isDockerAvailable(), "Docker is not available");
}
private static boolean isDockerAvailable() {
try {
DockerClientFactory.instance().client();
return true;
} catch (Exception e) {
return false;
}
}
}
Resource Management
@Testcontainers
class ResourceManagementTest {
@Container
private static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine");
@AfterEach
void cleanup() throws Exception {
// Clean up test data after each test
try (Connection conn = DriverManager.getConnection(
postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword());
Statement stmt = conn.createStatement()) {
stmt.execute("DROP TABLE IF EXISTS test_data");
}
}
}
Conclusion
Using Docker in JUnit tests provides powerful testing capabilities:
Key Benefits:
- Real Service Testing: Test against actual dependencies
- Isolation: Clean environment for each test
- Consistency: Same environment everywhere
- Complex Scenarios: Test multi-service interactions
- Easy Setup: Simple container management
Best Practices:
- Use container reuse for faster tests
- Clean up test data between tests
- Use parallel execution when possible
- Handle Docker availability gracefully
- Use shared containers for multiple test classes
When to Use Docker in Tests:
- Integration testing with real dependencies
- Testing service interactions
- Database migration testing
- Testing with external services
- Complex multi-service scenarios
Docker in JUnit tests bridges the gap between unit tests and full integration tests, providing reliable, isolated testing environments that closely match production.
Next Steps: Explore Testcontainers modules for specific services, implement container reuse strategies, and integrate with your CI/CD pipeline for reliable testing across all environments.