Ch. 8 · Spring Boot

Spring Boot Testcontainers

Test against real databases and services in containers, manage their lifecycle, and isolate state between tests.

~2 min readadvancedupdated Oct 5, 2026

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);
  }
}
java

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.

More in Spring Boot

read ✓Spring Boot · hard

Spring @Async and Executor Configuration

Run methods asynchronously with @Async, configure a bounded executor, and handle exceptions and the proxy boundary.

~2 min readread →
read ✓Spring Boot · hard

Spring Boot Caching Abstraction

Cache method results with @Cacheable, choose keys and TTLs, and evict on writes without the self-invocation trap.

~2 min readread →
read ✓Spring Boot · hard

Spring Declarative HTTP Clients

Define outbound HTTP as an annotated interface with @HttpExchange, create the proxy, and configure timeouts and errors.

~2 min readread →
esc