go-webtestというパッケージを作ってました
go-webtestというパッケージを作ってました。
go-webtest?
大雑把に言うと以下のようなものの詰め合わせです
- 手軽にresponseを取り出せるようなclient
- web apiのテスト時に発生するフラストレーションを緩和するような機能
- request/responseをtestdata以下にgoldenデータとして保存するsnapshot testing
そしてコンセプトは以下の通りです。
Sometimes, easy is better than simple
場合によっては便利な方がシンプルなことより嬉しいときがあったりしますね。そういう感じです。
APIのテストをしようとしたときにどういう体験をしたいかということを気にして作っていました。
手軽にsnapshot testingがしたい
例えば以下の様なAPIがあるとします(handlerの実装が知りたい人はこちら)。
$ http -b GET ":8080/api/add?x=10&y=20" { "ans": 30 }
まず成功例だけを書いてみることにします。以下の様なテストコードになります。
test_main.go
package main import ( "net/http" "testing" webtest "github.com/podhmo/go-webtest" "github.com/podhmo/go-webtest/try" ) func TestAPI(t *testing.T) { c := webtest.NewClientFromHandler(http.HandlerFunc(Add)) var want interface{} try.It{ Code: http.StatusOK, Want: &want, }.With(t, c, "GET", "/api/add?x=10&y=20") }
初回の実行結果を覚えて以降は以前の実行結果と比較します(初回は必ず成功します)
$ go test -v === RUN TestAPI --- PASS: TestAPI (0.00s) snapshot.go:51: save snapshot data: "testdata/TestAPI.golden" snapshot.go:56: load snapshot data: "testdata/TestAPI.golden" PASS ok m/00addtest 0.008s
例えばちょっと引数を変えて実行してみます。
diff --git a/handler_test.go b/handler_test.go index 8540d62..719b86b 100644 --- a/handler_test.go +++ b/handler_test.go @@ -14,5 +14,5 @@ func TestAPI(t *testing.T) { try.It{ Code: http.StatusOK, Want: &want, - }.With(t, c, "GET", "/api/add?x=10&y=20") + }.With(t, c, "GET", "/api/add?x=100&y=20") }
以下のように失敗します。
$ go test --- FAIL: TestAPI (0.00s) snapshot.go:56: load snapshot data: "testdata/TestAPI.golden" handler_test.go:17: on equal check: jsondiff, got and want is not same. status=NoMatch { "ans": 30 => 120 } left (want) : {"ans":30} right (got) : {"ans":120} FAIL exit status 1 FAIL m/00addtest 0.004s
snapshotを更新しましょう。テスト関数の名前がTestAPIだったのでそれの部分文字列を渡してあげればマッチしたテスト名のsnapshotが更新されます(SNAPSHOT=1
のときには全てのsnapshotを更新します)。
$ SNAPSHOT=API go test PASS ok m/00addtest 0.004s $ go test PASS ok m/00addtest 0.006s
今度は上手くいってます。このように記録されたものと比較して差分が出たらエラーにするようなものをsnapshot testingと言います。
似たようなものはpythonではcrvpyやsnapshottestなどが有ります。またsnapshot testingの雰囲気を掴みたい人にはjestのドキュメントなども良いかもしれません。
怠惰なassertionが殺意を招く
もうちょっとだけ続きます。今度はこちらの話です。
web apiのテスト時に発生するフラストレーションを緩和するような機能
これは単刀直入に言えばテストが失敗したときのエラーメッセージが豊かになっていて欲しいという話です。例えばAPIの変更の結果テストが壊れるとします。その壊れたテストを直すときにステータスコードだけ分かっても嬉しくなかったりします。
diff --git a/handler_test.go b/handler_test.go index 8540d62..e6a1ed0 100644 --- a/handler_test.go +++ b/handler_test.go @@ -14,5 +14,5 @@ func TestAPI(t *testing.T) { try.It{ Code: http.StatusOK, Want: &want, - }.With(t, c, "GET", "/api/add?x=10&y=20") + }.With(t, c, "GET", "/api/add?x=10&y=20&z=foo") }
例えば以下の様に。
$ go test --- FAIL: TestAPI (0.00s) handler_test.go:17: unexpected error, status code, got is "400 Bad Request", but want is "200 OK"
分かることがステータスコードだけというのはけっこうツライですね。幾つかある不正な入力の可能性から誤りであったrequestのパラメーターなどを特定しなければいけない。特に途中参加したプロジェクトなどでテストの実行に時間がかかっている上でそのような状況だとストレスがやばい(やばい)。フラストレーションが溜まります。
せめて発生したエラーなども出てきて欲しいですよね。あるいは利用したvalidationライブラリのvalidation errorだとか。
まともなフレームワークを使っていたりまともな環境でコードを書いていれば何らかのエラー原因がresponse bodyに含まれるはずです(ですよね?)。なのでそれをテストコードで潰してしまうのは。。。良くないことですね。。。良くないことなんです。
そんなわけでしっかりとresponse bodyも見えていて欲しい。
$ go test --- FAIL: TestAPI (0.00s) snapshot.go:56: load snapshot data: "testdata/TestAPI.golden" handler_test.go:17: unexpected error, status code, got is "400 Bad Request", but want is "200 OK" Response: ------------------------------ HTTP/1.1 400 Bad Request Connection: close {"error": "strconv.Atoi: parsing \"foo\": invalid syntax"} ---------------------------------------- FAIL exit status 1 FAIL m/00addtest 0.006s
万策尽きてのtrace
意図しないresponseが返ってきてしまうテストの原因は何なんでしょうね?あるいは変にラップされた便利ライブラリ(のようにみえる)コードに依存した処理のテストコードなどではどのようなrequestが投げられているのか把握しづらかったりします。
ストレスに精神がやられて頭を働かせたくない状況だとかあったりしますね。万策尽きた状態のときだとか。とりあえず DEBUG=1
と付けるとrequest/responseをtraceしてくれます。
$ DEBUG=1 go test 2019/09/18 00:03:40 builtin debug trace is activated === RUN TestAPI Request : ------------------------------ GET /api/add?x=10&y=20&z=foo HTTP/1.1 Host: example.com ---------------------------------------- Response: ------------------------------ HTTP/1.1 400 Bad Request Connection: close {"error": "strconv.Atoi: parsing \"foo\": invalid syntax"} ---------------------------------------- --- FAIL: TestAPI (0.00s) snapshot.go:56: load snapshot data: "testdata/TestAPI.golden" handler_test.go:17: unexpected error, status code, got is "400 Bad Request", but want is "200 OK" Response: ------------------------------ HTTP/1.1 400 Bad Request Connection: close {"error": "strconv.Atoi: parsing \"foo\": invalid syntax"} ---------------------------------------- FAIL exit status 1 FAIL m/00addtest 0.003s
あー、なるほど。query stringに数値だけではなく文字列が混ざっていますね。なるほどなるほど。ちなみにテストが成功しているときにも使えます(testing.TBを使わずstderrに出力している辺りはちょっと直したいかもなーと思ったりもしています)。
$ DEBUG=1 go test -v 2019/09/18 00:22:20 builtin debug trace is activated === RUN TestAPI Request : ------------------------------ GET /api/add?x=100&y=20 HTTP/1.1 Host: example.com ---------------------------------------- Response: ------------------------------ HTTP/1.1 200 OK Connection: close Content-Type: text/plain; charset=utf-8 {"ans": 120} ------------------------------------------- PASS: TestAPI (0.00s) snapshot.go:56: load snapshot data: "testdata/TestAPI.golden" PASS ok m/00addtest 0.004s
あとこのtraceはrouter側の"xxx handler is reached (matched)"などのdebug表示と組み合わさっていると404のときの原因を調べるのにも意外と便利な気がしています。
他にももう少し色々あるんですが今回はこのへんでおしまい。