Skip to content

Testing with JUnit 5

Start a fresh server when a test needs a different schema. When tests share the same schema and seed data, register one PgmemExtension and initialize its template once; then choose a fork scope for each test class.

@Test
void schemaVariant() throws Exception {
try (Pgmem pg = Pgmem.builder().database("app").start();
Connection conn = DriverManager.getConnection(pg.template().jdbcUrl())) {
conn.createStatement().execute(SCHEMA_V2);
// ...
}
}

Register the extension in a static field. JUnit starts pgmem before the class and calls prepare once with the template Server; run migrations and seed data there. When prepare returns, the extension snapshots that database. The test forks below start from this prepared snapshot, so migrations and seed data stay out of each test method and the test body can focus on its behavior.

class OrderRepositoryTest {
@RegisterExtension
static PgmemExtension pg = PgmemExtension.builder()
.database("app")
.prepare(t -> {
Flyway.configure().dataSource(t.jdbcUrl(), t.user(), null).load().migrate();
seed(t.jdbcUrl());
})
.build();
}

Nested classes reuse the outer class’s process.

Both scopes below start from this same prepared template. Choose class scope only when every test in the class is read-only; method scope gives each test its own writable copy.

Ask for ForkScope.CLASS on the parameter, or make it the default with forkScope(ForkScope.CLASS) on the builder.

@Test
void totals(@PgmemFork(scope = ForkScope.CLASS) DataSource ds) throws Exception {
try (Connection conn = ds.getConnection();
ResultSet rs = conn.createStatement().executeQuery("SELECT count(*) FROM orders")) {
rs.next();
assertEquals(10_000, rs.getInt(1));
}
}

A Fork, DataSource or @PgmemFork String parameter gets a fresh copy of the prepared database, closed after the test. Several parameters in one test share one fork.

@Test
void cancelOrder(DataSource ds) throws Exception {
new OrderRepository(ds).cancel(1);
try (Connection conn = ds.getConnection();
ResultSet rs = conn.createStatement().executeQuery("SELECT status FROM orders WHERE id = 1")) {
rs.next();
assertEquals("cancelled", rs.getString(1));
}
}
@Test
void withUrl(@PgmemFork String jdbcUrl) {
var app = new App(jdbcUrl);
// ...
}
@Test
void cancelOrder(Fork fork) throws Exception {
new OrderRepository(fork.dataSource()).cancel(1);
IDatabaseConnection conn = new DatabaseConnection(DriverManager.getConnection(fork.jdbcUrl()));
ITable actual = conn.createQueryTable("orders", "SELECT id, status FROM orders ORDER BY id");
ITable expected = new FlatXmlDataSetBuilder()
.build(getClass().getResourceAsStream("/after_cancel.xml")).getTable("orders");
Assertion.assertEquals(expected, actual);
}

template(name, prepare) adds a second prepared database, and @PgmemFork("name") selects it.

@RegisterExtension
static PgmemExtension pg = PgmemExtension.builder()
.database("app")
.prepare(t -> migrate(t.jdbcUrl()))
.template("audit", t -> migrateAudit(t.jdbcUrl()))
.build();
@Test
void auditTrail(@PgmemFork("audit") DataSource audit) { /* ... */ }

JUnit’s parallel execution works; forks are independent. maxForks(n) caps live forks per template (default: available processors). When all slots are occupied, a new fork waits. forkTimeout(Duration) makes that wait fail with pool_timeout; without it, the wait has no deadline.

import java.time.Duration;
@RegisterExtension
static PgmemExtension pg = PgmemExtension.builder()
.database("app")
.maxForks(4)
.forkTimeout(Duration.ofSeconds(30))
.build();

Every forked test JVM, from Gradle’s maxParallelForks or Surefire’s forkCount, starts its own binary and pool. See limits for snapshot, startup and connection-wait timeouts.