Go の基本
pgmem は Go のモジュールです。サーバーは自分のプロセスの中で動くので、インストールするバイナリも、実行時のダウンロードもありません。
インストール
Section titled “インストール”go get github.com/shibukawa/pgmem github.com/jackc/pgx/v5pgmem と、このページで使う一般的なドライバ pgx をまとめてインストールします。
テストで使う接続方法
Section titled “テストで使う接続方法”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 倍速くなります。ベンチマーク結果を参照してください。
-
サーバーを起動します。
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=disableOptionsにはほかにUser、Port(0 なら空きポート)、postgres -cの設定を渡すParams、サーバーログを受け取るLog、ほかの接続のアイドル状態のトランザクションの後ろで待てる時間を決めるWaitTimeoutがあります。 -
スキーマを登録します。 DSN に対してマイグレーションを流します。接続文字列を受け取るツールなら何でも使えます。下のマイグレーションツールを参照してください。
-
シードデータを入れます。 どのテストも前提にする行を投入します。シードデータを参照してください。
-
アプリケーションから接続します。 バックエンドのユニットテストでは、リポジトリやサービスが
*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 stringerr = conn.QueryRow(ctx, "SELECT name FROM users WHERE id = $1", 1).Scan(&name)pool, err := pgxpool.New(ctx, s.DSN())if err != nil {return err}defer pool.Close()import _ "github.com/jackc/pgx/v5/stdlib"db, err := sql.Open("pgx", s.DSN())if err != nil {return err}defer db.Close()
プロセス内ダイヤラで TCP を省く
Section titled “プロセス内ダイヤラで TCP を省く”Server.Dial はループバックソケットではなく net.Pipe で接続します。TCP の往復は時間の大半を PostgreSQL ではなくカーネルで使うので、小さなクエリならおよそ 3 倍速くなります。pgmemtest.Fixture.PgxPool と pgmemtest.Fixture.DB はこのダイヤラを設定してくれます。
cfg, _ := pgx.ParseConfig(s.DSN())cfg.DialFunc = s.Dialconn, _ := 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.Dialpool, _ := pgxpool.NewWithConfig(ctx, poolCfg)マイグレーションツール
Section titled “マイグレーションツール”マイグレーションツールに必要なのは DSN か *sql.DB だけです。テストに複製を渡す前に一度だけ流します。
//go:embed schema.sqlvar schema string
if _, err := db.ExecContext(ctx, schema); err != nil { return err}1 つの文字列に複数の文を書いてかまいません。
import ( "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file")
m, err := migrate.New("file://migrations", s.DSN())if err != nil { return err}if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { return err}import "github.com/pressly/goose/v3"
goose.SetDialect("postgres")if err := goose.Up(db, "migrations"); err != nil { // db は *sql.DB return err}out, err := exec.CommandContext(ctx, "atlas", "migrate", "apply", "--dir", "file://migrations", "--url", s.DSN()).CombinedOutput()if err != nil { return fmt.Errorf("atlas: %v: %s", err, out)}// GORMgdb, _ := gorm.Open(postgres.Open(s.DSN()), &gorm.Config{})gdb.AutoMigrate(&User{}, &Order{})
// entclient, _ := ent.Open("postgres", s.DSN())client.Schema.Create(ctx)シードデータ
Section titled “シードデータ”dbtestify は 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 testdatavar 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{})}_, err := db.ExecContext(ctx, ` INSERT INTO users (id, name, email) VALUES (1, 'Frank', 'frank@example.com'), (2, 'Grace', 'grace@example.com'); INSERT INTO orders (id, user_id, amount) VALUES (1, 1, 1200);`)rows := [][]any{{1, "Frank", "frank@example.com"}, {2, "Grace", "grace@example.com"}}_, err := conn.CopyFrom(ctx, pgx.Identifier{"users"}, []string{"id", "name", "email"}, pgx.CopyFromRows(rows))数千行を入れるなら COPY が最速です。
スナップショットとフォークを直接使う
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() はそれらが閉じるまで待ちます。