Testing with Go
pgmemtest.Fixture provides three common connection forms. Use its per-test helpers only for tests that need to write or otherwise need isolation; read-only tests can share one fork as described below. It also provides PgxConn(t) when code needs one standalone pgx connection:
| Handle | Method | Transport | Best for |
|---|---|---|---|
*pgxpool.Pool |
fx.PgxPool(t) |
In-process, no TCP | pgx-based application code; recommended for queries |
*sql.DB |
fx.DB(t) |
In-process, no TCP | database/sql, ORMs and tools that accept a DB handle |
| DSN | fx.DSN(t) |
Loopback TCP | Drivers and tools that require a connection URL |
The first two use Server.Dial over net.Pipe and avoid TCP overhead; the DSN remains the portable option for tools that need a URL. Each helper closes its handle and fork when the test ends.
Database lifecycle strategy
Section titled “Database lifecycle strategy”Use a fresh server when a test needs a different schema. When tests use the same schema and seed data, initialize a baseline once, then choose how each test uses it:
- Share one fork across a read-only package.
- For tests that write, reset a reusable fork between serial tests or fork a fresh copy per test.
The prepared baseline is the parent of both shared and per-test copies; migrations and seed data do not run again on each fork.
A fresh server per test
Section titled “A fresh server per test”The simplest shape: every test starts its own server and builds the schema. Nothing is shared, so it is also the slowest when migrations are long.
func TestSchemaVariant(t *testing.T) { s, err := pgmem.Start(t.Context(), pgmem.Options{Database: "app"}) if err != nil { t.Fatal(err) } t.Cleanup(func() { s.Close() })
db, err := sql.Open("pgx", s.DSN()) if err != nil { t.Fatal(err) } t.Cleanup(func() { db.Close() }) if _, err := db.Exec(schemaV2); err != nil { t.Fatal(err) } // ...}Prepare the baseline once
Section titled “Prepare the baseline once”pgmemtest.Run starts a template server, runs Prepare once, snapshots the result, runs the tests and cleans up. Put migrations and seed data in Prepare; its db handle is already connected to the template. Every later fork starts from that snapshot, so tests do not repeat the setup.
package store_test
import ( "context" "database/sql" "os" "testing"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/stdlib" "github.com/shibukawa/pgmem" "github.com/shibukawa/pgmem/pgmemtest")
var fx *pgmemtest.Fixture
func TestMain(m *testing.M) { os.Exit(pgmemtest.Run(m, pgmemtest.Options{ Options: pgmem.Options{Database: "app"}, Prepare: func(ctx context.Context, db *sql.DB, dsn string) error { if err := migrateUp(dsn); err != nil { // golang-migrate, goose, Atlas ... return err } return seed(ctx, db) // dbtestify, SQL, COPY ... }, }, func(f *pgmemtest.Fixture) { fx = f }))}Prepare receives a *sql.DB already connected in-process and the DSN for tools that want a URL. The migration and seed code from the basics goes here unchanged.
Share one fork across a read-only package
Section titled “Share one fork across a read-only package”If every test in a package only reads, create one fork from the prepared snapshot and share it. The fork starts with the schema and seed data from Prepare:
var fx *pgmemtest.Fixturevar shared *pgmem.Servervar readOnly *sql.DB
func TestMain(m *testing.M) { code := pgmemtest.Run(m, pgmemtest.Options{ Options: pgmem.Options{Database: "app"}, Prepare: func(ctx context.Context, db *sql.DB, dsn string) error { if err := migrateUp(dsn); err != nil { return err } return seed(ctx, db) }, }, func(f *pgmemtest.Fixture) { fx = f var err error shared, err = f.Snapshot().Fork(context.Background()) if err != nil { panic(err) } cfg, err := pgx.ParseConfig(shared.DSN()) if err != nil { panic(err) } cfg.DialFunc = shared.Dial readOnly = stdlib.OpenDB(*cfg) }) if readOnly != nil { _ = readOnly.Close() } if shared != nil { _ = shared.Close() } os.Exit(code)}
func TestReportTotals(t *testing.T) { t.Parallel() var total int if err := readOnly.QueryRow("SELECT sum(amount) FROM orders").Scan(&total); err != nil { t.Fatal(err) }}Reset or fork per test for tests that write
Section titled “Reset or fork per test for tests that write”For sequential tests that reuse a writable fork, close its connections and call Server.Reset(ctx) between tests to restore the prepared baseline. Do not reset a fork while parallel tests are using it. For isolated or parallel tests, prefer a fresh fork per test: the Fixture helpers below create it from the prepared snapshot and close it when the test ends.
func TestCreateOrder(t *testing.T) { t.Parallel() db := fx.DB(t) // in-process connection to a fresh copy
if _, err := db.Exec(`INSERT INTO orders (user_id, amount) VALUES (1, 500)`); err != nil { t.Fatal(err) }}func TestCreateOrder(t *testing.T) { t.Parallel() conn := fx.PgxConn(t) _, err := conn.Exec(t.Context(), `INSERT INTO orders (user_id, amount) VALUES (1, 500)`) if err != nil { t.Fatal(err) }}func TestService(t *testing.T) { t.Parallel() svc := NewService(fx.PgxPool(t)) // ...}func TestWithURL(t *testing.T) { t.Parallel() app := NewApp(Config{DatabaseURL: fx.DSN(t)}) // ...}fx.Fork(t) returns the *pgmem.Server itself when you need both DSN() and Dial.
Verify the resulting table contents
Section titled “Verify the resulting table contents”For tests of a public API, assert its observable response. For backend unit tests, checking persisted rows directly is another useful verification method: dbtestify compares the resulting table contents with an expected data set and reports a diff on mismatch.
import ( "github.com/shibukawa/dbtestify" "github.com/shibukawa/dbtestify/assertdb")
func TestCancelOrder(t *testing.T) { t.Parallel() db := fx.DB(t)
if err := CancelOrder(t.Context(), db, 1); err != nil { t.Fatal(err) } assertdb.AssertDBWithDB(t, db, dbtestify.PostgresDialect, testdata, "testdata/after_cancel.yaml", nil)}Parallelism and memory
Section titled “Parallelism and memory”Every live fork holds its own data-directory copy and buffer cache. MaxForks limits forks from one snapshot and defaults to GOMAXPROCS, which usually matches go test’s default parallelism. Set it on pgmemtest.Options or SnapshotOptions to trade memory for parallelism.
pgmemtest.Options{MaxForks: 4, /* ... */}When the cap is full, Fork(ctx) waits until a fork closes. Its context can cancel or deadline that wait; the pgmemtest helpers use the test’s context. With server logging enabled, a wait longer than five seconds is logged. See limits for how this differs from snapshot, reset and connection-wait timeouts.