t.Cleanup()で並列に動かすことのできるテーブルドリブンテストを書く
TL;DR
Goで並列に動くハンドラのテストを書くときに、起動したサーバーは t.Cleanup()
を使うといい感じに書くことが出来る。普段通り defer
を使うと変なところでハマるので注意しましょう。
Goで並列に動くテーブルドリブンテスト
最近は副業でGoを書いており、テストを書くときにはよくテーブルドリブンテストを書いています。
テストの数が多くなってくるとテストを動かすだけでも長い時間がかかるので、t.Parallel()
を使い、(DB等の同時にアクセスすると不整合が起きたりする場合などを除いては)できるだけ並列にテストを動かしています。
テーブルドリブンテストを並列で行うということについては、テストを変更してエラーが起きた際にテストコード起因なのか、それとも並列に動かしていることによって起因したものなのかがわかりづらいという論点があると思いますが、そこは考えずに今回は進めていきます。
A, Bという2つのテスト用関数がある時、A, Bを並列にテストするというのはあると思いますが、この中でテーブルドリブンテストを動かす際には、テーブルの情報によって作られたサブテストも並列に動かしたくなると思います。
テストデータをfor文で回して1回ずつテストするのではなく、あらかじめテーブルの情報からサブテストを作成しておき、並列でサブテストを全て回すということです。
func TestFunctionA(t *testing.T){
t.Parallel() // TestFunctionA を並列に動かす
type testCase struct {
name string
title string
expectStatusCode int
}
testCases := map[string]*testCase{
"Yurine-sanはアクセス可能": &testCase {
name: "Yurine",
title: "DROPKICK ON MY DEVIL",
expectStatusCode: http.StatusOK,
},
"Medusa-sanはアクセス可能": &testCase {
name: "Medusa",
title: "DROPKICK ON MY DEVIL",
expectStatusCode: http.StatusOK,
},
"zyashin-changはアクセスできない": &testCase {
name: "Zyashin",
title: "DROPKICK ON MY DEVIL",
expectStatusCode: http.StatusBadRequest,
},
}
for testName, tc := range testCases {
tc := tc
t.Run(testName, func(t *testing.T){
t.Parallel()
t.Log(tc)
})
}
}
func TestFunctionB(t *testing.T){
t.Parallel() // TestFunctionB を並列に動かす
type testCase struct {
name string
title string
expectStatusCode int
}
testCases := map[string]*testCase{
"Rin-sanはアクセス可能": &testCase {
name: "Rin",
title: "Yurucamp",
expectStatusCode: http.StatusOK,
},
"Ena-sanはアクセス可能": &testCase {
name: "Ena",
title: "Yurucamp",
expectStatusCode: http.StatusOK,
},
"Nadeshiko-changはアクセスできない": &testCase {
name: "Nadeshiko",
title: "Yurucamp",
expectStatusCode: http.StatusBadRequest,
},
}
for testName, tc := range testCases {
tc := tc
t.Run(testName, func(t *testing.T){
t.Parallel()
t.Log(tc)
})
}
}
このコードについては、特に何も意識せずに動くはずです。
=== RUN TestFunctionA
=== PAUSE TestFunctionA
=== RUN TestFunctionB
=== PAUSE TestFunctionB
=== CONT TestFunctionA
=== RUN TestFunctionA/zyashin-changはアクセスできない
=== PAUSE TestFunctionA/zyashin-changはアクセスできない
=== RUN TestFunctionA/Yurine-sanはアクセス可能
=== PAUSE TestFunctionA/Yurine-sanはアクセス可能
=== RUN TestFunctionA/Medusa-sanはアクセス可能
=== PAUSE TestFunctionA/Medusa-sanはアクセス可能
=== CONT TestFunctionB
=== RUN TestFunctionB/Rin-sanはアクセス可能
=== PAUSE TestFunctionB/Rin-sanはアクセス可能
=== RUN TestFunctionB/Ena-sanはアクセス可能
=== PAUSE TestFunctionB/Ena-sanはアクセス可能
=== RUN TestFunctionB/Nadeshiko-changはアクセスできない
=== PAUSE TestFunctionB/Nadeshiko-changはアクセスできない
=== CONT TestFunctionB/Rin-sanはアクセス可能
TestFunctionB/Rin-sanはアクセス可能: prog.go:75: &{Rin Yurucamp 200}
=== CONT TestFunctionB/Nadeshiko-changはアクセスできない
TestFunctionB/Nadeshiko-changはアクセスできない: prog.go:75: &{Nadeshiko Yurucamp 400}
=== CONT TestFunctionA/Medusa-sanはアクセス可能
TestFunctionA/Medusa-sanはアクセス可能: prog.go:39: &{Medusa DROPKICK ON MY DEVIL 200}
=== CONT TestFunctionA/Yurine-sanはアクセス可能
TestFunctionA/Yurine-sanはアクセス可能: prog.go:39: &{Yurine DROPKICK ON MY DEVIL 200}
=== CONT TestFunctionB/Ena-sanはアクセス可能
TestFunctionB/Ena-sanはアクセス可能: prog.go:75: &{Ena Yurucamp 200}
--- PASS: TestFunctionB (0.00s)
--- PASS: TestFunctionB/Rin-sanはアクセス可能 (0.00s)
--- PASS: TestFunctionB/Nadeshiko-changはアクセスできない (0.00s)
--- PASS: TestFunctionB/Ena-sanはアクセス可能 (0.00s)
=== CONT TestFunctionA/zyashin-changはアクセスできない
TestFunctionA/zyashin-changはアクセスできない: prog.go:39: &{zyashin DROPKICK ON MY DEVIL 400}
--- PASS: TestFunctionA (0.00s)
--- PASS: TestFunctionA/Medusa-sanはアクセス可能 (0.00s)
--- PASS: TestFunctionA/Yurine-sanはアクセス可能 (0.00s)
--- PASS: TestFunctionA/zyashin-changはアクセスできない (0.00s)
PASS
HTTPハンドラのテスト
勘が良い方ならテストケース等から「これはHTTPハンドラ周りのテストだな?」ということがわかると思います。
テスト手順を書くとするなら、以下のような手順になりそうです。
- テストをするハンドラを書く
- テストの中でリクエストを待つようなサーバーを起動して待ち受ける
-
for
文の中で起動しておいたサーバーに向かってリクエストを投げつける - responseに入っているステータスコードやBodyを使ってassertする
この通りに書いてみましょう。なお、今回の論点はサーバー周りの実装に対するものではないため、ある程度簡易的な実装とします。
テストをするハンドラを書く
実装については今回論点ではないため、ある程度簡素なものにします。
type Anime struct {
Title string `json:"title"`
Name string `json:"name"`
}
func getTestHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
w.WriteHeader(http.StatusNotImplemented)
return
}
b, err := ioutil.ReadAll(req.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
var anime Anime
err = json.Unmarshal(b, &anime)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if anime.Title == "DROPKICK ON MY DEVIL" {
if anime.Name == "Zyashin" {
w.WriteHeader(http.StatusUnauthorized)
} else {
w.WriteHeader(http.StatusOK)
w.Write([]byte(fmt.Sprintf("Hello %v San!", anime.Name)))
}
} else {
w.WriteHeader(http.StatusBadRequest)
}
return
}
}
getTestHandler()
を呼べばハンドラが返ってきます。これをテストごとに動かせば良いはずです。
func TestFunctionA(t *testing.T) {
t.Parallel() // TestFunctionA を並列に動かす
type testCase struct {
name string
title string
expectStatusCode int
}
testCases := map[string]*testCase{
"Yurine-sanはアクセス可能": &testCase{
name: "Yurine",
title: "DROPKICK ON MY DEVIL",
expectStatusCode: http.StatusOK,
},
"Medusa-sanはアクセス可能": &testCase{
name: "Medusa",
title: "DROPKICK ON MY DEVIL",
expectStatusCode: http.StatusOK,
},
"zyashin-changはアクセスできない": &testCase{
name: "Zyashin",
title: "DROPKICK ON MY DEVIL",
expectStatusCode: http.StatusUnauthorized,
},
}
ts := httptest.NewServer(getTestHandler())
defer ts.Close()
for testName, tc := range testCases {
tc := tc
t.Run(testName, func(t *testing.T) {
t.Parallel()
var u bytes.Buffer
u.WriteString(string(ts.URL))
anime := &Anime{Title: tc.title, Name: tc.name}
b, err := json.Marshal(anime)
if err != nil {
t.Fatalf("request json create failed: %v", err)
}
client := http.DefaultClient
defer client.CloseIdleConnections()
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewBuffer(b))
if err != nil {
t.Fatalf("error occured at create new request : %v", err)
}
res, err := client.Do(req)
if err != nil {
t.Fatalf("test api request error: %v", err)
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Fatalf("response read failed: %v", err)
}
if tc.expectStatusCode != res.StatusCode {
t.Fatalf("unexpected response status code: want %v, but got %v, response body: %v", tc.expectStatusCode, res.StatusCode, string(body))
}
var resdata Anime
err = json.NewDecoder(res.Body).Decode(&resdata)
if reflect.DeepEqual(anime, &resdata) {
t.Fatalf("unexpected server response want: %v but got %v, response body: %v", anime, &resdata, string(body))
}
})
}
}
これで動きそうですが、実は上手く動きません。
=== RUN TestFunctionA
=== PAUSE TestFunctionA
=== CONT TestFunctionA
=== RUN TestFunctionA/Medusa-sanはアクセス可能
=== PAUSE TestFunctionA/Medusa-sanはアクセス可能
=== RUN TestFunctionA/zyashin-changはアクセスできない
=== PAUSE TestFunctionA/zyashin-changはアクセスできない
=== RUN TestFunctionA/Yurine-sanはアクセス可能
=== PAUSE TestFunctionA/Yurine-sanはアクセス可能
=== CONT TestFunctionA/Medusa-sanはアクセス可能
=== CONT TestFunctionA/Yurine-sanはアクセス可能
=== CONT TestFunctionA/zyashin-changはアクセスできない
prog.go:108: test api request error: Post "http://127.0.0.1:30426": dial tcp 127.0.0.1:30426: connect: connection refused
=== CONT TestFunctionA/Yurine-sanはアクセス可能
prog.go:108: test api request error: Post "http://127.0.0.1:30426": dial tcp 127.0.0.1:30426: connect: connection refused
=== CONT TestFunctionA/Medusa-sanはアクセス可能
prog.go:108: test api request error: Post "http://127.0.0.1:30426": dial tcp 127.0.0.1:30426: connect: connection refused
--- FAIL: TestFunctionA (0.00s)
--- FAIL: TestFunctionA/zyashin-changはアクセスできない (0.00s)
--- FAIL: TestFunctionA/Yurine-sanはアクセス可能 (0.00s)
--- FAIL: TestFunctionA/Medusa-sanはアクセス可能 (0.00s)
FAIL
全部 connection refused
エラーになります。リクエストが上手く出来ていないようです。
並列に動かそうとしたテストでリクエストが上手くいかない原因を調査する
一度並列に動かすのをやめてみましょう。
テーブルからサブテストを生成する
for testName, tc := range testCases {
//tc := tc
t.Run(testName, func(t *testing.T) {
//t.Parallel()
この部分をコメントアウトします。
=== RUN TestFunctionA
=== PAUSE TestFunctionA
=== CONT TestFunctionA
=== RUN TestFunctionA/Yurine-sanはアクセス可能
=== RUN TestFunctionA/Medusa-sanはアクセス可能
=== RUN TestFunctionA/zyashin-changはアクセスできない
--- PASS: TestFunctionA (0.00s)
--- PASS: TestFunctionA/Yurine-sanはアクセス可能 (0.00s)
--- PASS: TestFunctionA/Medusa-sanはアクセス可能 (0.00s)
--- PASS: TestFunctionA/zyashin-changはアクセスできない (0.00s)
PASS
ok command-line-arguments 0.004s
並列にテストをしなくなるといい感じに動いているみたいです。
並列にしたいけれど、並列にすると何故か connection refused
になるという状況です。
connection refusedの原因として、一番に考えられるのはサーバーが立ち上がっていないという状況です。
どうやら
ts := httptest.NewServer(getTestHandler())
defer ts.Close()
ここで起動したサーバーが死んでいるようで、deferあたりが怪しいですね。
どうもサブテストが回る前にdefer
が実行され、サーバーが落ちているという状況みたいです。
というわけで簡単なテストのためのテストを書きました。
package main
import (
"log"
"testing"
)
func TestDefer(t *testing.T) {
tcs := map[string]string{
"a": "あ",
"b": "い",
"c": "う",
}
defer log.Printf("えお")
for tn, tc := range tcs {
tc := tc
t.Run(tn, func(t *testing.T) {
t.Parallel()
log.Println(tc)
})
}
}
ここで問題、上のようなparallelな環境下で、deferはどのタイミングで呼ばれるでしょうか?
Goのdeferは呼び出し元の関数がreturnするまで実行されません。普通に考えれば出力としては
あ
い
う
えお
となりそうです。これは defer
が最後の最後に実行されるということからも自然であると言えます。
しかし、実際の出力はこうでした
=== RUN TestDefer
=== RUN TestDefer/a
=== PAUSE TestDefer/a
=== RUN TestDefer/b
=== PAUSE TestDefer/b
=== RUN TestDefer/c
=== PAUSE TestDefer/c
2009/11/10 23:00:00 えお
=== CONT TestDefer/a
2009/11/10 23:00:00 あ
=== CONT TestDefer/b
2009/11/10 23:00:00 い
=== CONT TestDefer/c
2009/11/10 23:00:00 う
--- PASS: TestDefer (0.00s)
--- PASS: TestDefer/a (0.00s)
--- PASS: TestDefer/b (0.00s)
--- PASS: TestDefer/c (0.00s)
??????????????????
どうもdeferがサブテストに入る前に実行されているらしい。調べていると、同じようなことが issueに上がっているのが確認できました。
サブテストが実行される前にdeferが呼ばれてしまう
作成者は同様なテストを書いていて、deferが先に呼ばれている問題に遭遇していた模様。
その中でなされていた会話の一部で
t.Parallel is making the t.Run itself a parallel subtest of TestBug. There is no parallel subtest of the t.Run subtest. TestBug will not complete until the parallel test completes. But TestBug's body has to finish because that is what triggers running the parallel tests in the first place.
とある。サブテストが起動する前にdeferが呼ばれるのはどうも仕様らしい。
deferも上手く使えないみたいだし、どうしようと思っていたら大学の先輩が書いた記事がヒットした。どうやら t.Cleanup()
を使うことで解消することができるようです。
Cleanup registers a function to be called when the test and all its subtests complete. Cleanup functions will be called in last added, first called order.
t.Cleanup()
はサブテストが終わってからLIFOで呼び出されるらしい。この周りはdeferと同じ挙動をしているようです。
書き換えてみました。
package main
import (
"log"
"testing"
)
func TestDefer(t *testing.T) {
tcs := map[string]string{
"a": "あ",
"b": "い",
"c": "う",
}
t.Cleanup(func() {
log.Printf("えお")
})
for tn, tc := range tcs {
tc := tc
t.Run(tn, func(t *testing.T) {
t.Parallel()
log.Println(tc)
})
}
}
=== RUN TestDefer
=== RUN TestDefer/a
=== PAUSE TestDefer/a
=== RUN TestDefer/b
=== PAUSE TestDefer/b
=== RUN TestDefer/c
=== PAUSE TestDefer/c
=== CONT TestDefer/a
=== CONT TestDefer/b
=== CONT TestDefer/c
2009/11/10 23:00:00 あ
2009/11/10 23:00:00 い
2009/11/10 23:00:00 う
2009/11/10 23:00:00 えお
--- PASS: TestDefer (0.00s)
--- PASS: TestDefer/a (0.00s)
--- PASS: TestDefer/b (0.00s)
--- PASS: TestDefer/c (0.00s)
PASS
確かにdeferのときとは違い、サブテストが終了してから えお
が出力されており、期待していた挙動をしています。
これを元に、上のe2eテストっぽいものを書き換えてみましょう。
=== RUN TestFunctionA
=== PAUSE TestFunctionA
=== CONT TestFunctionA
=== RUN TestFunctionA/Yurine-sanはアクセス可能
=== PAUSE TestFunctionA/Yurine-sanはアクセス可能
=== RUN TestFunctionA/Medusa-sanはアクセス可能
=== PAUSE TestFunctionA/Medusa-sanはアクセス可能
=== RUN TestFunctionA/zyashin-changはアクセスできない
=== PAUSE TestFunctionA/zyashin-changはアクセスできない
=== CONT TestFunctionA/Yurine-sanはアクセス可能
=== CONT TestFunctionA/zyashin-changはアクセスできない
=== CONT TestFunctionA/Medusa-sanはアクセス可能
--- PASS: TestFunctionA (0.00s)
--- PASS: TestFunctionA/Yurine-sanはアクセス可能 (0.00s)
--- PASS: TestFunctionA/Medusa-sanはアクセス可能 (0.00s)
--- PASS: TestFunctionA/zyashin-changはアクセスできない (0.00s)
PASS
t.Cleanup()は1.14から入った機能で、割りと新しめのものとなっているがかゆいところに手が届く感じでとてもいいと思いました。
Discussion