Testing with JUnit 5
Database lifecycle strategies
Section titled “Database lifecycle strategies”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.
A fresh server per test
Section titled “A fresh server per test”@Testvoid schemaVariant() throws Exception { try (Pgmem pg = Pgmem.builder().database("app").start(); Connection conn = DriverManager.getConnection(pg.template().jdbcUrl())) { conn.createStatement().execute(SCHEMA_V2); // ... }}@Testfun schemaVariant() { Pgmem.builder().database("app").start().use { pg -> DriverManager.getConnection(pg.template().jdbcUrl()).use { conn -> conn.createStatement().execute(SCHEMA_V2) } }}Initialize one template once
Section titled “Initialize one template once”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();}class OrderRepositoryTest {
companion object { @JvmField @RegisterExtension val pg: PgmemExtension = 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.
Share one fork across a read-only class
Section titled “Share one fork across a read-only class”Ask for ForkScope.CLASS on the parameter, or make it the default with forkScope(ForkScope.CLASS) on the builder.
@Testvoid 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)); }}@Testfun totals(@PgmemFork(scope = ForkScope.CLASS) ds: DataSource) { ds.connection.use { conn -> conn.createStatement().executeQuery("SELECT count(*) FROM orders").use { rs -> rs.next() assertEquals(10_000, rs.getInt(1)) } }}Fork per method for tests that write
Section titled “Fork per method for tests that write”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.
@Testvoid 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)); }}
@Testvoid withUrl(@PgmemFork String jdbcUrl) { var app = new App(jdbcUrl); // ...}@Testfun cancelOrder(ds: DataSource) { OrderRepository(ds).cancel(1) ds.connection.use { conn -> conn.createStatement().executeQuery("SELECT status FROM orders WHERE id = 1").use { rs -> rs.next() assertEquals("cancelled", rs.getString(1)) } }}Assert with DbUnit
Section titled “Assert with DbUnit”@Testvoid 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);}Several templates
Section titled “Several templates”template(name, prepare) adds a second prepared database, and @PgmemFork("name") selects it.
@RegisterExtensionstatic PgmemExtension pg = PgmemExtension.builder() .database("app") .prepare(t -> migrate(t.jdbcUrl())) .template("audit", t -> migrateAudit(t.jdbcUrl())) .build();
@Testvoid auditTrail(@PgmemFork("audit") DataSource audit) { /* ... */ }Parallel runs
Section titled “Parallel runs”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;
@RegisterExtensionstatic 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.