Testcontainers

Real PostgreSQL in Every Test Run

Testcontainers starts a throwaway Docker container for the test run. Spring Boot 3.1+ wires it in with @ServiceConnection, no URL properties needed.

<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>

The Test

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)  // keep our DataSource
@Testcontainers
class OrderRepositoryIT {

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

    @Autowired OrderRepository orders;
    @Autowired TestEntityManager tem;

    @Test
    void flywayMigrationsMatchTheEntities() {
        // Flyway ran all V*.sql scripts; ddl-auto=validate passed, or the context would not start
        assertThat(orders.count()).isZero();
    }

    @Test
    void pickBatchSkipsLockedRows() {
        // uses FOR UPDATE SKIP LOCKED: only testable on a real database
        ...
    }
}

Run

./mvnw test -Dtest=OrderRepositoryIT

Example output

Creating container for image: postgres:16-alpine
Container postgres:16-alpine is starting: 3f2a9c...
Container postgres:16-alpine started in PT1.9S
Flyway: Successfully applied 18 migrations to schema "public", now at version v18
HHH000490: Using JtaPlatform implementation: [NoJtaPlatform]
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.41 s

Faster Runs

  • Share one container across test classes: put it in a base class or a @TestConfiguration with a @Bean @ServiceConnection method, and import that config.
  • Spring caches the application context between test classes with the same configuration, so the container starts once per run.
  • Locally, .withReuse(true) plus testcontainers.reuse.enable=true in ~/.testcontainers.properties keeps the container between runs.
  • CI needs Docker: GitHub Actions ubuntu-latest runners have it.