Closed9
testing.T.Cleanupとdeferの違いをメモ
発端
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.
サブテストまで含めて全部終わったタイミングで良い感じに呼ばれるということが書かれていたが気づいていなかった(追記)
いろいろ調べたり考えて観ると以下2つの利点がありそう
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
}
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にクローズされました