コンテンツにスキップ

Go のテストへの組み込み

クライアントの寿命や並列実行から方式を選ぶ場合は、分離方式の選択を参照してください。アプリ経由のサンプルでは、アプリが書いた結果を同じフォークから検証します。

すぐに試す:一度準備してテストを分離する

Section titled “すぐに試す:一度準備してテストを分離する”

ライブラリを追加し、次のテストファイルを保存して実行します。

ターミナルウィンドウ
go get github.com/shibukawa/pgmem/pgmemtest
go test ./...
store_test.go
package store_test
import (
"context"
"database/sql"
"os"
"testing"
"github.com/shibukawa/pgmem/pgmemtest"
)
var fx *pgmemtest.Fixture
func TestMain(m *testing.M) {
os.Exit(pgmemtest.Run(m, pgmemtest.Options{
Prepare: func(ctx context.Context, db *sql.DB, _ string) error {
_, err := db.ExecContext(ctx, "CREATE TABLE orders(id int)")
return err
},
}, func(f *pgmemtest.Fixture) { fx = f }))
}
func TestCreateOrder(t *testing.T) {
t.Parallel()
testDB := fx.For(t)
db := testDB.DB()
if _, err := db.Exec("INSERT INTO orders VALUES (1)"); err != nil {
t.Fatal(err)
}
// testDB.DSN() and testDB.PgxPool() use this same database.
}

fx.For(t) はフォークを 1 つ取得します。アプリと検証コードの両方で接続が必要なら、返されたオブジェクトを共有してください。DB()、PgxConn()、PgxPool()、DSN() はすべて同じフォークを指します。もう一度 For(t) を呼ぶと、別のデータベースを取得します。既存の fx.DB(t) などの直接呼び出すヘルパーは、呼ぶたびに新しいフォークを取得します。

起動方式 SQL の接続方法 パッケージ
テストプロセス内 既定は net.Pipe、ネットワーク接続は Transport: "tcp" / "unix" pgmemtest
所有する子プロセス TCP pgmemprocess
所有する子プロセス Unix domain socket pgmemprocess の Transport: "unix"

外部プロセスを使う場合は、バイナリを一度インストールします。

ターミナルウィンドウ
go install github.com/shibukawa/pgmem/cmd/pgmem@latest
go get github.com/shibukawa/pgmem/pgmemprocess

pgmem を PATH に置くか、PGMEM_BINARY または Binary でパスを指定します。上の quickstart の fixture の import と TestMain を次に置き換えてください。

import "github.com/shibukawa/pgmem/pgmemprocess"
var fx *pgmemprocess.Fixture
func TestMain(m *testing.M) {
os.Exit(pgmemprocess.Run(m, pgmemprocess.FixtureOptions{
Options: pgmemprocess.Options{
Database: "app",
Transport: "tcp", // "unix" where AF_UNIX stream sockets are supported
},
Prepare: func(ctx context.Context, db *sql.DB, _ string) error {
_, err := db.ExecContext(ctx, "CREATE TABLE orders(id int)")
return err
},
}, func(f *pgmemprocess.Fixture) { fx = f }))
}

テスト本体はそのまま使えます。pgmemprocess と pgmemfixture の import では、組み込みエンジンをテストバイナリにコンパイル・リンクしません。子プロセスの制御には標準入出力、SQL には選択した接続方式を使います。

Unix モードでは、既定で Unix は /tmp、Windows は os.TempDir() の下に専用ディレクトリを作ります。必要なら、既に存在する短い親パスを SocketDir で指定してください。テンプレートと各フォークのディレクトリは別々です。reset では接続先を維持し、正常終了時に削除します。ディスクに置くのは socket のエントリだけで、データベースのファイルはメモリに保持します。AF_UNIX の stream socket に対応する Windows でも使えます。datagram socket は使いません。AF_UNIX 非対応ホストでは listen 時にエラーになります。強制終了時には socket が残る場合があるため、そのプロセスが使っていた専用ディレクトリだけを削除してください。

Windows の stream 対応と datagram の制限は、Microsoft と Go の資料を参照してください。

StartupTimeout、SnapshotTimeout、ForkTimeout は既定で 30 秒、ShutdownTimeout は 10 秒です。ほかのテストがプールを占有すると fork の取得がタイムアウトします。使い終えたフォークを閉じるか、MaxForks を調整してください。共有フォークも 1 枠使います。

読み取り専用テストでは、fx.SharedDB() と fx.SharedPgxPool() が準備済みフォークを共有し、fixture が接続を閉じます。できればセットアップ時に取得してください。SharedReadOnly: true で、これらの接続からの通常の永続データへの書き込みを防げます。既定では書き込みが可能で、ほかの共有テストにも影響します。生の接続先と独立した For(t) の接続は書き込み可能です。

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 を使います。Transport を tcp または unix に設定すると、これらのヘルパーも指定したネットワーク接続に切り替わります。DSN は URL を必要とするツールに使えます。どのヘルパーも、テスト終了時に接続とフォークを閉じます。

アプリのデータソース設定を変えずに使う

Section titled “アプリのデータソース設定を変えずに使う”

アプリが通常 DATABASE_URL、または libpq の PGHOST、PGPORT、PGUSER、PGDATABASE を読むなら、リポジトリやプールを作る前に fx.ShadowPG(t) を呼びます。準備済みスナップショットからフォークを作り、テスト中だけ接続環境を切り替えます。アプリは普段どおりのドライバとデータソースを作ります。

user_repository_test.go
func TestCreateUser(t *testing.T) {
fx.ShadowPG(t) // テスト専用フォーク。リポジトリの設定は変更しない
repo, err := NewUserRepository() // 通常どおり DATABASE_URL を読む
if err != nil { t.Fatal(err) }
t.Cleanup(func() { repo.Close() })
if err := repo.Create(t.Context(), "ada@example.com"); err != nil { t.Fatal(err) }
if got, err := repo.FindByEmail(t.Context(), "ada@example.com"); err != nil || got == nil || got.Email != "ada@example.com" {
t.Fatalf("user = %v, %v", got, err)
}
}

独自の DSN 変数を使うアプリには fx.ShadowPG(t, pgmemtest.ShadowOptions{ExtraEnv: []string{"APP_DATABASE_URL"}}) を指定します。Shared: true は読み取り専用テストで 1 つのフォークを共有します。このフォークへの書き込みは後続テストにも残ります。testing.T.Setenv はプロセス全体の環境を変更するため、このテストと親テストでは t.Parallel() を使えません。並列テストには fx.DB(t) または fx.PgxPool(t) を渡してください。

Go には登録済みの database/sql ドライバや、作成済みの *sql.DB・pgx プールを差し替える公式の仕組みがありません。ShadowPG は呼び出し後にアプリが作る接続に効きます。同じ設定を HTTP サーバーで使う例は API テスト と E2E テスト を参照してください。

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

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 で用意したスキーマとシードデータが入っています。

readOnly, err := fx.SharedDB()
if err != nil {
panic(err) // acquire during TestMain setup
}
// Use readOnly in read-only tests; fx.Close() owns its pool and fork.
// fx.SharedPgxPool() uses the same fork.

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

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 つのスナップショットから作れるフォーク数を制限します。既定値は利用可能なメモリの 4 分の 1 をフォーク 1 個のコスト(shared_buffers + 約 32MB)で割った値で、7GB の CI ランナーなら 28 です。go test が同時に走らせるテスト数は -parallel(既定は GOMAXPROCS)で決まるため、CPU 以外を待つテストが多いならそちらも上げてください。pgmemtest.Options または SnapshotOptions で設定し、メモリ使用量と並列数を調整できます。

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

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

同じフォークのドライバを設定する

Section titled “同じフォークのドライバを設定する”

TestDB の callback で runtime parameter や pool の上限を指定できます。接続先の識別情報と dialer は fixture の設定を維持します。外部プロセスと Unix 接続でも同じです。

testDB := fx.For(t)
pool := testDB.PgxPool(func(cfg *pgxpool.Config) {
cfg.MaxConns = 4
cfg.ConnConfig.RuntimeParams["application_name"] = "orders-test"
})
db := testDB.DB(func(cfg *pgx.ConnConfig) {
cfg.RuntimeParams["statement_timeout"] = "1000"
})
// pool and db connect to the same fork.

ShadowPG の安全なワーカー並列化

Section titled “ShadowPG の安全なワーカー並列化”

go test -p 4 ./... はパッケージごとに別のテストバイナリを起動するため、環境変数と TestMain の fixture がプロセスごとに分かれます。1 パッケージ内の ShadowPG テストは逐次実行します。同じパッケージを複数ワーカーへ分けるなら、重複しない -run 条件で go test -run ... -count=1 を複数プロセス起動します。-parallel 1 を指定しても、t.Parallel() を呼ぶテスト内では t.Setenv を使えません。1 プロセス内で並列化したい場合は、ShadowPG ではなくテストごとの fx.DB(t) または fx.PgxPool(t) を注入します。