Closed9

testing.T.Cleanupとdeferの違いをメモ

podhmopodhmo

godoc

情報がない。

package testing // import "testing"

func (c *T) Cleanup(f func())
    Cleanup registers a function to be called when the test (or subtest) and all
    its subtests complete. Cleanup functions will be called in last added, first
    called order.
podhmopodhmo

サブテストまで含めて全部終わったタイミングで良い感じに呼ばれるということが書かれていたが気づいていなかった(追記)

podhmopodhmo

deferの呼び出しをsetup関数側に持っていける

これはsignatureから。引数として *testing.T を受け取るのでテスト用のファクトリー関数を別途作ったときにエラー対応を中に閉じ込めておけるのと同様にcleanup処理も中に閉じ込めておける。

例えば、func(...)(T, func() error, error) 型のファクトリー関数を func(...) T として扱えるようにできる。

素直に既存のファクトリー関数を使った場合

仮にDBにアクセスするためのDB structが必要になるとする。このファクトリー関数をそのまま使ったコードは以下のようになる。errorも返せばcleanup関数も返すファクトリー関数なのでやばい。

func TestUseDirectly(t *testing.T) {
	db, cleanup, err := NewDB()
	if err != nil {
		t.Fatalf("unexpected error (new db): %+v", err)
	}
	defer func() {
		if err := cleanup(); err != nil {
			t.Errorf("unexpected error (close db): %+v", err)
		}
	}()

	// use db
	if db == nil {
		t.Errorf("must not be nil")
	}
}

ここで ファクトリー関数の定義は以下のようなもの

type DB struct{}

func (db *DB) Close() error {
	return nil
}

type Option func(*DB) error

func NewDB(options ...Option) (*DB, func() error, error) {
	db := &DB{}
	for _, opt := range options {
		if err := opt(db); err != nil {
			return nil, func() error { return nil }, err
		}
	}
	return db, func() error { return db.Close() }, nil
}

t.Cleanup()を使った場合

ただの値を得るだけのファクトリー関数をテスト用のfixture(広義の意味)として作成しておける。使うときは以下のような形だけで済むので便利。


func TestUseWithFixture(t *testing.T) {
	db := NewTestDB(t)

	// use db
	if db == nil {
		t.Errorf("must not be nil")
	}
}

このときのfixtureの定義は以下


func NewTestDB(t *testing.T, options ...Option) *DB {
	t.Helper()
	db, cleanup, err := NewDB(options...)
	if cleanup != nil {
		t.Cleanup(func() {
			if err := cleanup(); err != nil {
				t.Errorf("unexpected error (close db): %+v", err)
			}
		})
	}
	if err != nil {
		t.Fatalf("unexpected error (new db): %+v", err)
	}
	return db
}
podhmopodhmo

t.Parallel()で覆われたsubtestの終了を待って親のtestのcleanupを呼ぶ

例えば、以下のようなsubtestを並行して実行したい場合のテストがある。ここで親の関数のdeferはsubtestの終了を待たずして呼ばれる。
(イメージ的にはgoroutineを動かしたときの子々のgoroutineの終了を待たないのと同様)

func TestDefer(t *testing.T) {
	// t.Run()で囲ったgoroutineより早くdeferが呼ばれてしまう
	// see: https://github.com/golang/go/issues/17791
	defer func() { t.Logf("end") }()

	for i := 0; i < 2; i++ {
		i := i
		t.Run("case"+strconv.Itoa(i), func(t *testing.T) {
			t.Parallel()
			defer t.Logf("end: %d", i)
			t.Logf("start: %d", i)
			time.Sleep(1 * time.Second)
		})
	}
}

これを以下のように、t.Cleanup()を利用する形に変えたとき、subtestの終了を待ってくれる。

func TestCleanup(t *testing.T) {
	t.Cleanup(func() { t.Logf("end") })

	for i := 0; i < 2; i++ {
		i := i
		t.Run("case"+strconv.Itoa(i), func(t *testing.T) {
			t.Parallel()
			t.Cleanup(func() { t.Logf("end: %d", i) })
			t.Logf("start: %d", i)
			time.Sleep(1 * time.Second)
		})
	}
}

実行ログ

TestDeferとTestCleanupのendの位置が異なる。

// === RUN   TestDefer
// === RUN   TestDefer/case0
// === PAUSE TestDefer/case0
// === RUN   TestDefer/case1
// === PAUSE TestDefer/case1
// === CONT  TestDefer
//     main_test.go:10: end           <- this
// === CONT  TestDefer/case0
//     main_test.go:16: start: 0
// === CONT  TestDefer/case1
//     main_test.go:16: start: 1
// === CONT  TestDefer/case0
//     main_test.go:18: end: 0
// === CONT  TestDefer/case1
//     main_test.go:18: end: 1
// --- PASS: TestDefer (0.00s)
//     --- PASS: TestDefer/case0 (1.00s)
//     --- PASS: TestDefer/case1 (1.00s)
//
//
// === RUN   TestCleanup
// === RUN   TestCleanup/case0
// === PAUSE TestCleanup/case0
// === RUN   TestCleanup/case1
// === PAUSE TestCleanup/case1
// === CONT  TestCleanup/case0
//     main_test.go:29: start: 0
// === CONT  TestCleanup/case1
//     main_test.go:29: start: 1
// === CONT  TestCleanup/case0
//     main_test.go:28: end: 0
// === CONT  TestCleanup/case1
//     main_test.go:28: end: 1
// === CONT  TestCleanup
//     main_test.go:23: end           <- this
// --- PASS: TestCleanup (0.00s)
//     --- PASS: TestCleanup/case0 (1.00s)
//     --- PASS: TestCleanup/case1 (1.00s)
このスクラップは2023/07/03にクローズされました