Testcontainers starts real dependencies, such as a database or a message broker, in disposable Docker containers for tests. That gives integration tests the real behavior of the dependency, catching the SQL and protocol issues that an in-memory substitute hides.
Before you start
You should be comfortable with Spring tests and Docker. This article covers container-backed integration testing.
Step-by-step walkthrough
Step 1: Start a real dependency for the tests
A @Container on a PostgreSQLContainer starts a real database, and Spring can wire its dynamic URL into the datasource. Tests run against the same engine as production, so dialect and constraint behavior match.
Step 2: Manage lifecycle and reuse
Containers can start per class or be reused across the suite to cut startup cost. A shared container is faster but must be isolated at the schema level, since several tests share it.
Step 3: Isolate state between tests
Each test needs a clean state. Reset data with a transaction rollback or by recreating the schema per test class. Sharing a container without resetting leaves state that makes tests order-dependent.
Worked scenario
The test runs against a real database container.
@Testcontainers
@SpringBootTest
class OrderRepositoryTest {
@Container
static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16");
@DynamicPropertySource
static void props(DynamicPropertyRegistry r) {
r.add("spring.datasource.url", db::getJdbcUrl);
r.add("spring.datasource.username", db::getUsername);
r.add("spring.datasource.password", db::getPassword);
}
}Walk through the example
The container starts a real PostgreSQL, and the dynamic properties point the datasource at it, so the repository test exercises real SQL. The container is static, so it starts once for the class and stops after. Properly resetting data between tests keeps them independent.
Common mistake
Using an in-memory database with different behavior from production, so passing tests hide real failures. Another is sharing a container without resetting state, so tests interfere. Ignoring container startup time can also make the suite slow.
Verify the behavior
Run a query that behaves differently on the in-memory substitute and confirm the container test catches it. Run tests in random order and confirm isolation. Measure startup time and confirm reuse reduces it.
Interview exercise
Why is a real database container better than an in-memory substitute?
Answer and reasoning
Because the in-memory substitute implements a subset of the real engine’s behavior, so dialect, constraints and functions can differ. A test that passes in memory can fail in production. A container runs the real engine, so the test exercises the same behavior users get.
Continue learning
Compare integration testing in Database tests and slices in Repository test slices. Read the Testcontainers documentation and try the Spring Boot interview questions.