コンテンツにスキップ

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.Dialnet.Pipe を使って TCP のオーバーヘッドを避けます。DSN は URL を必要とするツールに使える汎用的な方法です。どのヘルパーも、テスト終了時に接続とフォークを閉じます。

データベースのライフサイクル

Section titled “データベースのライフサイクル”

テストごとにスキーマを変えるなら、新しいサーバーを起動します。同じスキーマとシードデータを使うテストでは、基準となるデータベースを一度だけ初期化し、テストごとの使い方を選びます。

  • 読み取り専用のパッケージでは、1 つのフォークを共有します。
  • 書き込みを行うテストでは、直列実行ならフォークをリセットし、テストごとに分離するなら新しい複製を作ります。

共有用とテストごとの複製は、どちらも準備済みの基準データベースから作ります。フォークのたびにマイグレーションやシードデータの投入を繰り返すことはありません。

いちばん素朴な形です。テストごとにサーバーを起動し、スキーマを作ります。何も共有しない代わりに、マイグレーションが長いといちばん遅くなります。

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

書き込みテストではリセットするかテストごとにフォークする

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

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

生きているフォークはそれぞれデータディレクトリとバッファキャッシュの複製を持ちます。MaxForks は 1 つのスナップショットから作れるフォーク数を制限し、既定値は go test の既定の並列度と同じ GOMAXPROCS です。pgmemtest.Options または SnapshotOptions で設定し、メモリ使用量と並列数を調整できます。

pgmemtest.Options{MaxForks: 4, /* ... */}

上限に達すると、Fork(ctx) はフォークが閉じて枠が空くまで待ちます。context のキャンセルや期限で待機を中断できます。pgmemtest のヘルパーはテストの context を使います。サーバーログを有効にしている場合、5 秒を超える待機はログに記録されます。スナップショット、リセット、接続待ちのタイムアウトとの違いは制限事項を参照してください。