Tutorial: Learn TestContainers from Scratch (2026)
Integration tests used to be the bane of my existence — spinning up databases, message queues, and caches required complex setup scripts and led to flaky tests that passed on one machine but failed on another. TestContainers solved this by managing Docker containers programmatically from within my test code.
TestContainers is a Java library that provides lightweight, disposable instances of databases, message brokers, web servers, and any other Dockerized service for integration testing. In this tutorial, you will learn how to use TestContainers to write reliable integration tests that run anywhere Docker is installed.
Setting Up TestContainers and Your First Container
Add the TestContainers dependency to your pom.xml or build.gradle. The core library includes generic container support. Separate modules provide pre-configured containers for databases like PostgreSQL, MySQL, and MongoDB.
Your first test starts a PostgreSQL container, creates a JDBC connection, and runs a query. TestContainers manages the container lifecycle — it starts before the test and shuts down after. The JDBC URL is dynamically assigned based on the container's mapped port, eliminating hardcoded connection strings.
@Test
void shouldQueryDatabase() {
try (PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:15")) {
postgres.start();
String jdbcUrl = postgres.getJdbcUrl();
// Use jdbcUrl to connect and run queries
assertTrue(postgres.isRunning());
}
}
Database Integration Testing with TestContainers
TestContainers provides specialized modules for popular databases. The JDBC URL support automatically starts a container when the URL contains a TC parameter. This integrates seamlessly with frameworks like Spring Boot, Hibernate, and Flyway.
I use the @Container annotation with JUnit 4 or the @Testcontainers annotation with JUnit 5 for declarative container management. Database migrations run before each test class using Flyway or Liquibase. Each test gets a clean database state, eliminating test order dependencies.
@Testcontainers
class UserRepositoryTest {
@Container
static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Test
void shouldSaveUser() {
DataSource ds = DataSourceBuilder.create()
.url(postgres.getJdbcUrl())
.username(postgres.getUsername())
.password(postgres.getPassword())
.build();
// Test repository logic
}
}
Testing Message Queues and Caches
TestContainers supports containers for Kafka, RabbitMQ, Redis, and Elasticsearch. These modules configure the container with sensible defaults for testing. For Kafka, create topics and produce/consume messages within your test.
I test event-driven microservices by starting a Kafka container, producing an event, and verifying that the consumer processes it correctly. The container's network and ports are isolated per test run. This catches integration issues between services without deploying to a shared environment.
@Test
void shouldProcessKafkaEvent() {
try (KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0"))) {
kafka.start();
String bootstrapServers = kafka.getBootstrapServers();
// Create producer, send event
// Create consumer, verify event received
}
}
Custom Containers and Docker Compose
When a pre-built module does not exist, use GenericContainer to run any Docker image. Configure exposed ports, environment variables, commands, and wait strategies. The wait strategy ensures the container is ready before tests execute.
For multi-service setups, DockerComposeContainer reads a docker-compose.yml file and starts all services together. This is ideal for testing microservice architectures. I use this to spin up an entire service mesh — databases, caches, and dependent APIs — in a single test.
GenericContainer> container = new GenericContainer<>("nginx:alpine")
.withExposedPorts(80)
.waitingFor(Wait.forHttp("/"));
container.start();
String url = "http://" + container.getHost() + ":" + container.getMappedPort(80);
// Docker Compose
DockerComposeContainer> compose = new DockerComposeContainer<>(new File("docker-compose.yml"))
.withExposedService("db", 5432);
Network Isolation and Reusable Containers
TestContainers supports custom Docker networks for isolated communication between containers. Create a network and attach containers to it. This enables container-to-container communication using hostnames defined in the network.
Reusable containers persist between test runs for faster execution. Use withReuse(true) to keep the container running after the test. This speeds up local development significantly but should be disabled in CI to ensure clean state. I enable reuse locally and disable it in CI via system properties.
Network network = Network.newNetwork();
PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:15")
.withNetwork(network)
.withNetworkAliases("db");
RedisContainer redis = new RedisContainer("redis:7")
.withNetwork(network)
.withNetworkAliases("cache");
TestContainers Best Practices
Always define a container startup timeout for slow CI environments. Use Singleton containers for read-only databases shared across tests — create them once in a @BeforeAll method. For stateful databases, use fresh containers per test class to avoid contamination.
Resource management is critical — containers consume memory and CPU. Limit container resources with withCreateContainerCmdModifier. In CI, run tests sequentially to avoid resource starvation. Monitor Docker disk usage and prune unused images regularly.
@Test
void timeoutConfiguration() {
PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:15")
.withStartupTimeout(Duration.ofMinutes(5));
}
// Resource limits
postgres.withCreateContainerCmdModifier(cmd ->
cmd.withHostConfig(
new HostConfig().withMemory(512 * 1024 * 1024L)
)
);
Frequently Asked Questions
Do I need Docker installed to use TestContainers?
Yes, TestContainers requires a Docker daemon. On Linux, it connects to the local socket. On macOS and Windows, it uses Docker Desktop. CI environments should have Docker installed or use Docker-in-Docker setups.
Can TestContainers work with Podman instead of Docker?
TestContainers supports Podman through the TC_PODMAN environment variable or by configuring the Docker socket to point to Podman's socket. Some features like Docker Compose may have limited support.
How do I debug a failing TestContainers test?
Enable debug logging for org.testcontainers to see container startup logs. Use container.execInContainer() to run commands inside the container for inspection. Set TC_REUSE to true and inspect the running container manually.
What is the difference between TestContainers and H2 in-memory database?
H2 emulates PostgreSQL but has differences in SQL syntax, functions, and behavior. TestContainers runs the real PostgreSQL in Docker, giving you production-equivalent behavior. The trade-off is slower startup time.
Originally published on Ayodhyyya. Last updated June 1, 2026.