Reusable Testcontainers Postgres Base Class for Spring Boot
An abstract @Testcontainers base class that boots a Postgres container once per test suite and wires it into Spring Boot via @DynamicPropertySource.
PostgresTestContainerBase is the abstract class I extend in every Spring Boot integration test. It boots a single Postgres container once per JVM, wires its connection details into Spring's data source properties, and exposes a clean inheritance point. Drop it in, extend it, and your @SpringBootTest runs against a real Postgres in milliseconds per test.
Tested on Spring Boot 3.4, JDK 25, Testcontainers 1.20+.
When to Use This
- Replacing in-memory H2 with a real Postgres in your integration tests
- Catching SQL portability issues that H2 hides (JSONB, window functions,
ON CONFLICT) - Sharing one container across many test classes for speed
- Running CI integration tests that require database-specific behavior
Don't use this when your tests are pure unit tests with no SQL (mock the repository) or when you genuinely need a clean database per test (use container per class instead, with a different base).
Code
@Testcontainers
@SpringBootTest
public abstract class PostgresTestContainerBase {
@Container
static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test")
.withReuse(true); // singleton container across runs
static {
POSTGRES.start();
}
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
registry.add("spring.datasource.username", POSTGRES::getUsername);
registry.add("spring.datasource.password", POSTGRES::getPassword);
registry.add("spring.jpa.hibernate.ddl-auto", () -> "create-drop");
}
}The static block starts the container before Spring's @DynamicPropertySource runs, which guarantees the JDBC URL is available the moment Spring starts wiring beans. withReuse(true) is the magic that keeps the container alive across mvn test runs locally — set testcontainers.reuse.enable=true in ~/.testcontainers.properties.
Usage
A repository test that extends the base:
class UserRepositoryTest extends PostgresTestContainerBase {
@Autowired
UserRepository repo;
@Test
void shouldUpsertOnConflict() {
var user = new User("rabi@example.com", "Rabi");
repo.upsert(user);
repo.upsert(user); // second call should not throw
assertThat(repo.findByEmail("rabi@example.com")).isPresent();
}
}class OutboxPublisherTest extends PostgresTestContainerBase {
@Autowired OutboxPublisher publisher;
@Autowired JdbcTemplate jdbc;
@Test
void shouldPublishPendingEvents() {
jdbc.update("INSERT INTO outbox (aggregate_type, aggregate_id, event_type, payload) VALUES (?,?,?,?::jsonb)",
"Order", "123", "OrderCreated", "{}");
publisher.drain();
Integer remaining = jdbc.queryForObject(
"SELECT COUNT(*) FROM outbox WHERE published_at IS NULL",
Integer.class);
assertThat(remaining).isZero();
}
}Both test classes share the same Postgres container, started exactly once.
Caveats
- Container reuse needs to be enabled per developer machine. Add
testcontainers.reuse.enable=trueto~/.testcontainers.properties. Without it,withReuse(true)is ignored and a new container starts every run. ddl-auto=create-dropresets the schema each time Spring starts. That's what you want for test isolation, but it slows down tests if your schema is huge. For very large schemas, use Flyway/Liquibase migrations andvalidateinstead.- The
@Testcontainersannotation is fromorg.testcontainers.junit.jupiter, notorg.testcontainers. Easy import mistake. - CI must have Docker available. GitHub Actions ubuntu-latest has it pre-installed. Self-hosted runners may not.
- Don't share state between tests via the database. Even with one container, each test should set up its own data and clean up after, or use
@Transactionalon the test class so changes roll back automatically.
Related Snippets & Reading
- Outbox Publisher in Spring Boot — the canonical thing to test against this base
- Spring Boot Testcontainers Guide — the deep dive on why H2 is dangerous
- Testcontainers Postgres docs — official module reference
Frequently Asked Questions
Why use a base class instead of @Testcontainers per test?
A base class with a static container reuses the same Postgres instance across every test class that extends it, which is dramatically faster than spinning a new container per test class. Combined with the singleton container pattern, you get one container per JVM run.
How does Spring Boot pick up the dynamic Postgres connection details?
@DynamicPropertySource lets you register property values at runtime, before the Spring context starts. The container exposes its random JDBC URL and credentials, and the base class wires those into spring.datasource.* before any bean is created.