コンテンツにスキップ

Go の基本

pgmem は Go のモジュールです。サーバーは自分のプロセスの中で動くので、インストールするバイナリも、実行時のダウンロードもありません。

ターミナルウィンドウ
go get github.com/shibukawa/pgmem github.com/jackc/pgx/v5

pgmem と、このページで使う一般的なドライバ pgx をまとめてインストールします。

pgmemtest.Fixture には 3 つの接続方法があります。テストごとのヘルパーを使うと、呼び出したテスト専用のデータベースに接続できます。読み取り専用のテストでは、代わりに 1 つのフォークを共有できます。クエリには *pgxpool.Pool または *sql.DB を使うのがおすすめです。どちらも TCP ではなくプロセス内の net.Pipe で接続します。PostgreSQL の URL を要求するツールやクライアントには DSN を使います。

取得するもの Fixture のメソッド 接続方法
*pgxpool.Pool fx.PgxPool(t) プロセス内接続。pgx を使うアプリにおすすめ
*sql.DB fx.DB(t) プロセス内接続。database/sql を使うアプリにおすすめ
DSN fx.DSN(t) ループバック TCP。URL を受け取るあらゆるクライアントに使える

Go のテストガイドでは、データベースを一度準備し、読み取り専用テストではフォークを共有し、書き込みテストだけに専用の複製を渡す方法を説明しています。小さなクエリではプロセス内ダイヤラが TCP 接続より約 3 倍速くなります。ベンチマーク結果を参照してください。

  1. サーバーを起動します。 pgmem.Start は、専用のメモリ上データディレクトリを持つ PostgreSQL を起動して返します。

    s, err := pgmem.Start(ctx, pgmem.Options{Database: "app"})
    if err != nil {
    log.Fatal(err)
    }
    defer s.Close()
    fmt.Println(s.DSN()) // postgres://postgres@127.0.0.1:54321/app?sslmode=disable

    Options にはほかに UserPort(0 なら空きポート)、postgres -c の設定を渡す Params、サーバーログを受け取る Log、ほかの接続のアイドル状態のトランザクションの後ろで待てる時間を決める WaitTimeout があります。

  2. スキーマを登録します。 DSN に対してマイグレーションを流します。接続文字列を受け取るツールなら何でも使えます。下のマイグレーションツールを参照してください。

  3. シードデータを入れます。 どのテストも前提にする行を投入します。シードデータを参照してください。

  4. アプリケーションから接続します。 バックエンドのユニットテストでは、リポジトリやサービスが *sql.DB または *pgxpool.Pool を受け取る形にして、pgmemtest.Fixture から取得した接続を注入すると、ループバック TCP を通らないプロセス内ダイヤラを使えます。以下の例はドライバ単体の API を示すため DSN を使っています。DSN は E2E テスト、別プロセス、または PostgreSQL の URL を必要とするツールとの境界で使います。

    conn, err := pgx.Connect(ctx, s.DSN())
    if err != nil {
    return err
    }
    defer conn.Close(ctx)
    var name string
    err = conn.QueryRow(ctx, "SELECT name FROM users WHERE id = $1", 1).Scan(&name)

プロセス内ダイヤラで TCP を省く

Section titled “プロセス内ダイヤラで TCP を省く”

Server.Dial はループバックソケットではなく net.Pipe で接続します。TCP の往復は時間の大半を PostgreSQL ではなくカーネルで使うので、小さなクエリならおよそ 3 倍速くなります。pgmemtest.Fixture.PgxPoolpgmemtest.Fixture.DB はこのダイヤラを設定してくれます。

cfg, _ := pgx.ParseConfig(s.DSN())
cfg.DialFunc = s.Dial
conn, _ := pgx.ConnectConfig(ctx, cfg)
// database/sql では設定を登録して名前で開く
name := stdlib.RegisterConnConfig(cfg)
db, _ := sql.Open("pgx", name)
// pgxpool では設定を読み込み、同じダイヤラを指定します。
poolCfg, _ := pgxpool.ParseConfig(s.DSN())
poolCfg.ConnConfig.DialFunc = s.Dial
pool, _ := pgxpool.NewWithConfig(ctx, poolCfg)

マイグレーションツールに必要なのは DSN か *sql.DB だけです。テストに複製を渡す前に一度だけ流します。

//go:embed schema.sql
var schema string
if _, err := db.ExecContext(ctx, schema); err != nil {
return err
}

1 つの文字列に複数の文を書いてかまいません。

dbtestify は YAML のデータセットをシードとして投入できます。バックエンドのユニットテストでは、実行後のテーブル内容を期待するデータセットと比較し、差分を確認する用途にも使えます。

testdata/seed.yaml
users:
- { id: 1, name: Frank, email: frank@example.com }
- { id: 2, name: Grace, email: grace@example.com }
orders:
- { id: 1, user_id: 1, amount: 1200 }
import "github.com/shibukawa/dbtestify"
//go:embed testdata
var testdata embed.FS
func seed(ctx context.Context, db *sql.DB) error {
f, err := testdata.Open("testdata/seed.yaml")
if err != nil {
return err
}
defer f.Close()
data, err := dbtestify.ParseYAML(f)
if err != nil {
return err
}
dbc, err := dbtestify.NewDBConnectorFromDB(db, dbtestify.PostgresDialect)
if err != nil {
return err
}
return dbtestify.Seed(ctx, dbc, data, dbtestify.SeedOpt{})
}

スナップショットとフォークを直接使う

Section titled “スナップショットとフォークを直接使う”

次のページのテストヘルパーは、次の 2 つの呼び出しを包んだものです。直接使うこともできます。

snap, err := s.Snapshot(ctx, pgmem.SnapshotOptions{MaxForks: 8})
if err != nil {
return err
}
defer snap.Close()
fork, err := snap.Fork(ctx) // スナップショットの複製で動く新しいサーバー
if err != nil {
return err
}
defer fork.Close()

スナップショットはサーバーをチェックポイントしてデータディレクトリを複製します。フォークはそれをさらに複製し、新しいバックエンドを起動します。Snapshot は開いているトランザクションを待つので、その前に s への接続はすべてコミットするか閉じてください。

別のスナップショットの状態で稼働中サーバーを置き換えるには Restore を使います。データベース名とユーザーが同じスナップショットに限ります。フォークのポートとクライアント接続はそのままなので、すでに取得した DSN やプールを使い続けられます。

if err := fork.Restore(ctx, anotherSnapshot); err != nil {
return err
}

Reset(ctx) は、そのフォークを作ったスナップショットへ戻す短縮形です。Snapshot.Close() は新しいフォークを止めますが、稼働中のフォークは閉じません。Snapshot.Wait() はそれらが閉じるまで待ちます。