Testcontainers — Integration Testing with Docker
Unit tests mock dependencies. But sometimes you need to test against a real database or real message broker. Testcontainers spins up Docker containers for your tests and tears them down automatically.
Maven Dependencies
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.20.0</version>
<scope>test</scope>
</dependency>
<!-- Add module for your database/service -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.20.0</version>
<scope>test</scope>
</dependency>
Prerequisites
- Docker must be running on the machine where tests execute.
- Works with Docker Desktop, Colima, Rancher Desktop, Podman (with config).
PostgreSQL Integration 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.*;
import static org.junit.jupiter.api.Assertions.*;
@Testcontainers
class UserRepositoryIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Test
void shouldInsertAndRetrieveUser() throws SQLException {
try (Connection conn = DriverManager.getConnection(
postgres.getJdbcUrl(),
postgres.getUsername(),
postgres.getPassword())) {
// Setup
conn.createStatement().execute(
"CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(100))");
conn.createStatement().execute(
"INSERT INTO users (name) VALUES ('Alice')");
// Query
ResultSet rs = conn.createStatement().executeQuery(
"SELECT name FROM users WHERE id = 1");
assertTrue(rs.next());
assertEquals("Alice", rs.getString("name"));
}
}
}
How It Works
1. @Testcontainers annotation activates the extension
2. @Container fields are started before tests
3. Docker pulls the image (first time) and starts a container
4. Container gets a random port (avoids conflicts)
5. Tests run against the real service
6. Container is stopped and removed after tests
Spring Boot Integration
@SpringBootTest
@Testcontainers
class UserServiceIT {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private UserService userService;
@Test
void shouldCreateUser() {
User user = userService.createUser("Alice", "alice@test.com");
assertNotNull(user.getId());
User found = userService.findById(user.getId());
assertEquals("Alice", found.getName());
}
}
@DynamicPropertySourceinjects the container’s random port/URL into Spring’s configuration.
Redis Container
@Testcontainers
class CacheServiceIT {
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
@Test
void shouldCacheAndRetrieve() {
String host = redis.getHost();
int port = redis.getFirstMappedPort();
// Use Jedis, Lettuce, or any Redis client
try (Jedis jedis = new Jedis(host, port)) {
jedis.set("key", "value");
assertEquals("value", jedis.get("key"));
}
}
}
Kafka Container
@Testcontainers
class KafkaProducerIT {
@Container
static KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("confluentinc/cp-kafka:7.6.0"));
@Test
void shouldProduceAndConsumeMessage() {
String bootstrapServers = kafka.getBootstrapServers();
// Configure producer/consumer with bootstrapServers
// Send and receive messages...
}
}
Shared Containers (Singleton Pattern)
Starting a container per test class is slow. Share across tests:
public abstract class AbstractIntegrationTest {
static final PostgreSQLContainer<?> POSTGRES;
static {
POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb");
POSTGRES.start(); // started once, reused across all test classes
}
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
registry.add("spring.datasource.username", POSTGRES::getUsername);
registry.add("spring.datasource.password", POSTGRES::getPassword);
}
}
class UserServiceIT extends AbstractIntegrationTest {
// Uses the shared PostgreSQL container
}
class OrderServiceIT extends AbstractIntegrationTest {
// Same container — fast!
}
Docker Compose Support
@Testcontainers
class FullStackIT {
@Container
static DockerComposeContainer<?> environment =
new DockerComposeContainer<>(new File("src/test/resources/docker-compose-test.yml"))
.withExposedService("postgres", 5432)
.withExposedService("redis", 6379);
@Test
void shouldWorkWithMultipleServices() {
String pgHost = environment.getServiceHost("postgres", 5432);
int pgPort = environment.getServicePort("postgres", 5432);
// ...
}
}
CI/CD: Docker-in-Docker
GitHub Actions
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Run integration tests
run: mvn verify -Pfailsafe
# Docker is available by default on ubuntu-latest
Available Modules
| Module | Image |
|---|---|
postgresql |
PostgreSQL |
mysql |
MySQL |
mongodb |
MongoDB |
kafka |
Apache Kafka |
elasticsearch |
Elasticsearch |
localstack |
AWS services (S3, SQS, etc.) |
rabbitmq |
RabbitMQ |
mssqlserver |
SQL Server |
redis (via GenericContainer) |
Redis |
Common Mistakes
| Mistake | Fix |
|---|---|
| Docker not running | Start Docker Desktop before tests |
| Slow tests (container per class) | Use singleton pattern for shared containers |
| Hardcoded ports | Use getFirstMappedPort() — ports are random |
| Image not found | Check Docker image name and tag |
Key Takeaways
- Testcontainers = real services in Docker for integration tests.
@Testcontainers+@Container= automatic lifecycle management.- Use
@DynamicPropertySourcefor Spring Boot integration. - Singleton pattern shares containers across test classes for speed.
- Docker must be available — works in GitHub Actions out of the box.
What’s Next?
Next: Testing Best Practices — naming conventions, anti-patterns, test organization, and how to write tests that last.