Go のテストへの組み込み
pgmemtest.Fixture から、代表的な 3 種類の接続を取得できます。fx.*(t) のテストごとのヘルパーは、書き込みや分離が必要なテストだけで使います。読み取り専用のテストは、後述のように 1 つのフォークを共有できます。単独の pgx 接続が必要なら PgxConn(t) も使えます。
| 取得するもの | メソッド | 接続方法 | 向いている用途 |
|---|---|---|---|
*pgxpool.Pool |
fx.PgxPool(t) |
プロセス内、TCP なし | pgx を使うアプリのクエリ。おすすめ |
*sql.DB |
fx.DB(t) |
プロセス内、TCP なし | database/sql、ORM、DB ハンドルを受け取るツール |
| DSN | fx.DSN(t) |
ループバック TCP | 接続 URL を必要とするドライバやツール |
最初の 2 つは Server.Dial と net.Pipe を使って TCP のオーバーヘッドを避けます。DSN は URL を必要とするツールに使える汎用的な方法です。どのヘルパーも、テスト終了時に接続とフォークを閉じます。
データベースのライフサイクル
Section titled “データベースのライフサイクル”テストごとにスキーマを変えるなら、新しいサーバーを起動します。同じスキーマとシードデータを使うテストでは、基準となるデータベースを一度だけ初期化し、テストごとの使い方を選びます。
- 読み取り専用のパッケージでは、1 つのフォークを共有します。
- 書き込みを行うテストでは、直列実行ならフォークをリセットし、テストごとに分離するなら新しい複製を作ります。
共有用とテストごとの複製は、どちらも準備済みの基準データベースから作ります。フォークのたびにマイグレーションやシードデータの投入を繰り返すことはありません。
テストごとに新しいサーバー
Section titled “テストごとに新しいサーバー”いちばん素朴な形です。テストごとにサーバーを起動し、スキーマを作ります。何も共有しない代わりに、マイグレーションが長いといちばん遅くなります。
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) } // ...}基準データベースを一度だけ準備する
Section titled “基準データベースを一度だけ準備する”pgmemtest.Run はテンプレートサーバーを起動し、Prepare を一度だけ実行して、その結果をスナップショットにしてからテストを走らせます。マイグレーションとシードデータの投入は Prepare に書きます。ここで渡される db はテンプレートに接続済みです。以降のフォークはそのスナップショットから作られるため、テストごとに初期化を繰り返す必要はありません。
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 には、プロセス内で接続済みの *sql.DB と、URL を必要とするツール向けの DSN が渡されます。基本で紹介したマイグレーションとシードのコードは、そのままここに置けます。
読み取り専用のパッケージで 1 つのフォークを共有する
Section titled “読み取り専用のパッケージで 1 つのフォークを共有する”パッケージ内のテストがすべて読み取り専用なら、準備済みスナップショットからフォークを 1 つ作り、共有できます。フォークには 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) }}書き込みテストではリセットするかテストごとにフォークする
Section titled “書き込みテストではリセットするかテストごとにフォークする”直列で実行するテストが書き込み後も同じフォークを使う場合は、接続を閉じてからテストの間に Server.Reset(ctx) を呼び、準備済みの基準状態に戻します。並列テストが使っているフォークをリセットしてはいけません。テストを分離したり並列実行したりする場合は、以下の Fixture ヘルパーでテストごとに新しいフォークを作ります。ヘルパーは終了時にフォークも閉じます。
func TestCreateOrder(t *testing.T) { t.Parallel() db := fx.DB(t) // 新しい複製へのプロセス内接続
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)}) // ...}DSN() と Dial の両方が要るときは、fx.Fork(t) で *pgmem.Server そのものを受け取れます。
結果のテーブル内容を検証する
Section titled “結果のテーブル内容を検証する”公開 API のテストでは、まず API から観察できる応答を検証します。バックエンドのユニットテストでは、データベースに保存された行を直接確認する方法も使えます。dbtestify は実行後のテーブル内容を期待するデータセットと比較し、食い違いがあれば差分を表示します。
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)}並列度とメモリ
Section titled “並列度とメモリ”生きているフォークはそれぞれデータディレクトリとバッファキャッシュの複製を持ちます。MaxForks は 1 つのスナップショットから作れるフォーク数を制限し、既定値は go test の既定の並列度と同じ GOMAXPROCS です。pgmemtest.Options または SnapshotOptions で設定し、メモリ使用量と並列数を調整できます。
pgmemtest.Options{MaxForks: 4, /* ... */}上限に達すると、Fork(ctx) はフォークが閉じて枠が空くまで待ちます。context のキャンセルや期限で待機を中断できます。pgmemtest のヘルパーはテストの context を使います。サーバーログを有効にしている場合、5 秒を超える待機はログに記録されます。スナップショット、リセット、接続待ちのタイムアウトとの違いは制限事項を参照してください。