Skip to content

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.

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.

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)
}
// ...
}

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.

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.Fixture
var shared *pgmem.Server
var 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)
}
}

fx.Fork(t) returns the *pgmem.Server itself when you need both DSN() and Dial.

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

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.