0時に寝て7時に起きた。前日は久しぶりに飲んでいい気分で寝てた。朝コンビニ寄ったらいつもほぼ満席のイートインが閑散としているなと思いつつ、オフィスへ行って始業報告して、今日祝日だよとツッコミもらった。全然気付いていなかった。まだ新しい天皇誕生日に慣れてない。

go のテストにおける setup/teardown 関数の実装

昨日から結合テストのリファクタリングをしていてせっかくオフィスに行ったので続きをする。結合テストだとデータを作ったり削除したりをテスト単位にやりたい。例えば、ユーザーを作成してテスト実行し、終わったら作成したユーザーを削除したい。次のように setup 関数を書き、返り値として teardown の関数を返す。このときに t.Helper() を呼び出しておくと、エラーがあったときにどの呼び出し元のテストでエラーが発生したかがわかる。

func setupUsers(t *testing.T, users []mongodb.User) func() {
	t.Helper()
	user := mongodb.NewUserCollection(url, username, password)
	for _, u := range users {
		if err := user.Save(u.ID, u.Attributes); err != nil {
			t.Fatalf("failed to create a user: %s", err)
		}
	}
	return func() {
		t.Helper()
		if err := user.Truncate(); err != nil {
			t.Fatalf("failed to truncate the user collection: %s", err)
		}
	}
}

使い方は簡単で setup 関数を呼び出すと teardown 関数が返ってくるのでそれを defer で呼び出すとよい。

teardown := setupUsers(t, usersTestData)
defer teardown()

しかし、この setup 関数をサブテストと一緒に並行実行すると意図したように動かない。どうやらサブテストを並行実行すると親テストが呼ばれた後に呼び出されるという仕組みになっていて、サブテストを実行する前に teardown が呼ばれてしまうので意図した制御にならない。How to handle parent test teardown with parallel subtests in golang の回答 によると、サブテストからさらにサブテストを実行するとよいとある。この場合、親テストとサブテストは同期的に実行され、サブテストの中でさらにサブテストを並行実行するという考え方で意図した振る舞いになる。並行実行するときは次のようなコードになる。

func TestMy(t *testing.T) {
    tests := []struct{
        // test cases
    }

	teardown := setupUsers(t, usersTestData)
	defer teardown()

	t.Run("wrap for setup process", func(t *testing.T) {
		for _, tt := range tests {
			tt := tt
			t.Run(tt.name, func(t *testing.T) {
				t.Parallel()
                // do test
            })
        }
    })
}