Start Learning
Javaneer
Back to stage
Module 5·Testing Spring Applications

Integration Tests with Testcontainers

Run tests against the real database engine in a throwaway Docker container - no more H2-vs-production drift.

18 min readAdvanced
On this page

BookVault runs on H2 in tests but PostgreSQL in production. That gap is a trap: a native query, a specific column type, or a migration can pass on H2 and break on Postgres. Testcontainers closes the gap by running your tests against the real database - spun up in a throwaway Docker container and torn down afterward.

Add it

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>

@ServiceConnection wires it for you

Declare a PostgreSQL container and annotate it with @ServiceConnection. Spring Boot reads the running container's host, port, and credentials and points the datasource at it automatically - no manual URL configuration:

@SpringBootTest
@Testcontainers
class LendingIntegrationTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");

    @Autowired LendingService lending;

    @Test
    void borrowingReducesAvailabilityOnRealPostgres() {
        // Flyway migrations ran on a real Postgres; exercise the full stack…
    }
}

On startup the container boots, Flyway runs your migrations against real PostgreSQL, and your test runs end-to-end on the same engine production uses. When the test class finishes, the container is destroyed.

A crash test with the real car, not a clay model

Manufacturers don't certify safety by crashing a clay model - they crash the actual production car, because only the real materials reveal how it behaves on impact. Testcontainers crashes the real car: your migrations and queries run against genuine PostgreSQL, so behavior that a stand-in (H2) would fake is exercised for real - and any H2-vs-Postgres difference surfaces in your test, not in production.

Reuse the container across tests

Starting a container costs a few seconds. Declaring it static (as above) starts it once per test class and shares it across the methods, keeping integration tests reasonably fast. In CI, the Docker daemon provides the containers automatically.

Why H2 wasn't enough

BookVault's migrations and JPA worked against H2 through the whole project. Give one concrete reason a test on H2 could pass while the app breaks on production PostgreSQL, and explain how a Testcontainers integration test would have caught it.

What problem do Testcontainers integration tests primarily solve?

Key takeaways

  • Testcontainers runs integration tests against the real database engine in a throwaway Docker container, then destroys it.
  • It eliminates the H2-vs-production drift that lets tests pass while the app fails on the real database.
  • @ServiceConnection auto-configures Spring's datasource from the running container - no manual URL/credentials.
  • Flyway migrations run against the real engine in the container, so your schema is validated for real.
  • Declare the container static to start it once per class; CI provides the Docker daemon.
Was this lesson helpful?
Edit this page on GitHub