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のときの原因を調べるのにも意外と便利な気がしています。
他にももう少し色々あるんですが今回はこのへんでおしまい。
gitで更新のあったファイルに対してformatter(goimports)をかける
CIでlintのついでにformatter(gofmt, goimports, gofumpt)がかかっているかチェックしている環境があるとする。そこでformatされていないと怒られたファイルに対してformatterをかけたい。その方法のメモ。
あるコミットで変更されたファイルを集める
対象のコミットで変更されたファイルの一覧を集めてformatterにかけたい
git show --stat HEAD
をparseしてみる?
例えば対象のコミットをHEADということにすると git show --stat HEAD
の変更を利用するということが考えられる
以下の様な形。
$ git show --stat HEAD | grep -F '|' xxx/gen/mock/handler.go | 3 +- yyy/gen/mock/router.go | 3 +- zzz/gen/mock/next_state.go | 5 +- aws/s3/gen/mock/s3.go | 3 +- .../aws/aws-sdk-go/service/sqs/sqsifacemocks/SQSAPI.go | 122 +++++++++++------------ # 対象のファイルの一覧 $ git show | grep -F '|' | cut -d '|' -f 1 xxx/gen/mock/handler.go yyy/gen/mock/router.go zzz/gen/mock/next_state.go aws/s3/gen/mock/s3.go .../aws/aws-sdk-go/service/sqs/sqsifacemocks/SQSAPI.go
あとは一覧が取れればすんなり渡して行けば良いということに一見なりそうなのだけれど。そうはならない。ものによってはファイル名の出力が省略されてしまうことがある。
.../aws/aws-sdk-go/service/sqs/sqsifacemocks/SQSAPI.go
これが完全なファイルパスではないのでちょっと不便。
暫定的で頑健な普段の手癖
普段はあまり考えずにとりあえずbasenameだけあっていれば対象にしちゃって良いでしょ、と以下の様なコードを手癖で書いていたけれどもう少しまともなワンライナーを検討しても良いかもしれない。
for i in $(git show --stat HEAD | grep '|' | cut -d '|' -f 1); do find . -name "$(basename $i)"; done | xargs goimports -w
同一のbasenameのものが含まれるのでformatterに渡されるファイルが増える可能性はあるのだけれど、実用上はあまり問題にならない。まぁでももう少しキレイな方法を見出したい。
--stat=<column>
manを覗いてみると以下のようなことが書かれている。デフォルトのターミナルのcolum sizeは80でその値を元に良い感じに表示を調節しているよう。
$ man git-show ... --stat[=<width>[,<name-width>[,<count>]]] Generate a diffstat. By default, as much space as necessary will be used for the filename part, and the rest for the graph part. Maximum width defaults to terminal width, or 80 columns if not connected to a terminal, and can be overridden by <width>. The width of the filename part can be limited by giving another width <name-width> after a comma. The width of the graph part can be limited by using --stat-graph-width=<width> (affects all commands generating a stat graph) or by setting diff.statGraphWidth=<width> (does not affect git format-patch). By giving a third parameter <count>, you can limit the output to the first <count> lines, followed by ... if there are more. These parameters can also be set individually with --stat-width=<width>, --stat-name-width=<name-width> and --stat-count=<count>.
なのでテキトウに --stat
に大きめの数値を渡してあげれば大丈夫かもしれない。
$ git show --stat=100000000 HEAD | grep -F '|' | cut -d '|' -f 1 ... test/mocks/github.com/aws/aws-sdk-go/service/sqs/sqsifacemocks/SQSAPI.go
まぁ暫定的には。。
--name-status
たまたまmanをのぞいて見つけた --name-status
オプションの方が目的にはあっているかもしれない。
$ man git-show ... --name-status Show only names and status of changed files. See the description of the --diff-filter option on what the status letters mean.
以下の様な出力になる。変更されたもの(M)や追加されたもの(A)だけを取り出せるのでこちらのほうが良いかもしれない。
$ git show --name-status HEAD ... M xxx/gen/mock/handler.go M yyy/gen/mock/router.go M zzz/gen/mock/next_state.go M aws/s3/gen/mock/s3.go M test/mocks/github.com/aws/aws-sdk-go/service/sqs/sqsifacemocks/SQSAPI.go
一応これで大丈夫か調べてみる。
$ git log --stat | grep -F '...' | head -n 1 .../aws-sdk-go/service/sqs/sqsifacemocks/SQSAPI.go | 122 ++++++++++----------- $ git log --stat=1000000 | grep -F '...' | head -n 1 $ git log --name-status | grep -F '...' | head -n 1
大丈夫そう。
現在変更されているファイル
ついでに現在変更されているファイルにも使えるか試してみる(git reset HEAD~~
などでテキトウにそういう状況を再現して)。
現在変更されているファイルを対象に一覧したい場合。
$ git diff --name-status
addされたファイルのみを対象にしたい場合。
$ git diff --staged --name-status
というわけで一覧が欲しかったら以下の様な形にすれば良さそう。
$ git diff --name-status | grep -P '^(A|M)\t' | cut -f 2
まとめ
更新されたファイルの一覧が欲しかったら --name-status
が便利。git show
に限らず色々なコマンドで使える。