Testcontainers is a library that runs real Docker containers during integration tests. If you have been writing compose.yml files to set up test dependencies, Testcontainers does the same thing — but programmatically, from your test code.
Why Testcontainers
| Approach |
Pros |
Cons |
| Mocks/Stubs |
Fast, no dependencies |
May miss real integration issues |
| Shared test DB |
Simple |
Tests interfere with each other |
| Docker Compose |
Realistic |
Manual setup, not test-aware |
| Testcontainers |
Realistic + test-aware |
Slightly slower than mocks |
How It Works
Test Suite Start
│
├─→ Testcontainers starts a PostgreSQL container
├─→ Testcontainers starts a Redis container
├─→ Connection URLs are injected into your app config
│
├─→ Tests run against real databases
│
└─→ Containers are automatically destroyed
Java Example (JUnit 5)
@Testcontainers
class UserServiceTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
@Test
void shouldCreateUser() {
// postgres.getJdbcUrl() returns the dynamic connection URL
// redis.getHost() + redis.getMappedPort(6379) for Redis
// Write your test using real databases!
}
}
Python Example (pytest)
import testcontainers.postgres
import testcontainers.redis
def test_with_real_databases():
with testcontainers.postgres.PostgresContainer("postgres:16-alpine") as postgres:
with testcontainers.redis.RedisContainer("redis:7-alpine") as redis:
# postgres.get_connection_url() returns dynamic URL
# redis.get_container_host_ip() + redis.get_exposed_port(6379)
# Your test code here
assert True
Node.js Example
const { PostgreSqlContainer } = require('@testcontainers/postgresql');
const { GenericContainer } = require('testcontainers');
describe('API Tests', () => {
let pgContainer, redisContainer;
beforeAll(async () => {
pgContainer = await new PostgreSqlContainer('postgres:16-alpine').start();
redisContainer = await new GenericContainer('redis:7-alpine')
.withExposedPorts(6379)
.start();
});
afterAll(async () => {
await pgContainer.stop();
await redisContainer.stop();
});
test('health check returns ok', async () => {
// Use pgContainer.getConnectionUri() for DB connection
// Use redisContainer.getHost() + redisContainer.getMappedPort(6379)
});
});
Testcontainers vs Compose for Testing
| Feature |
Compose |
Testcontainers |
| Setup |
docker compose up before tests |
Automatic in test code |
| Cleanup |
Manual docker compose down |
Automatic |
| Port management |
Fixed ports (conflicts) |
Random ports (no conflicts) |
| Parallelism |
Hard (port conflicts) |
Easy (isolated containers) |
| CI/CD |
Extra step in pipeline |
Built into test step |
| Language |
Language-agnostic |
Language-specific libraries |
Getting Started
# Java (Maven)
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
# Python
pip install testcontainers
# Node.js
npm install --save-dev testcontainers
Tip: You do not have to choose one or the other. Use Compose for local development environments and Testcontainers for automated integration tests. They solve related but different problems.
Quick Reference
| Item |
Detail |
| Java library |
org.testcontainers:testcontainers |
| Python library |
pip install testcontainers |
| Node.js library |
npm install testcontainers |
| Supported DBs |
PostgreSQL, MySQL, MongoDB, Redis, and many more |
| Key feature |
Automatic container lifecycle tied to test lifecycle |