httptest.ResponseRecorderを使っていてもRoundTripperを使いたい
時折、テスト中にRoundTripper (client.Transport) を挟んで良い結果を得ようと企む事がある。コレに対応可能なのがhttptest.Serverを利用した通信のときだけだと思っていたけれど。一手間加えるとRecorderを利用したテストでも対応可能な事に気づいたのでメモ。
Recorder, Server?
httptestを使ったhttp requestのテストを書くときには2つの方針がある
- Recorderを使ったもの
- Serverを使ったもの
前者は基本的にはnet/http.HandlerFuncなどを直接関数として利用するもの。後者は適当なポートでローカルに本物のサーバーを立てて通信を行うもの。前者はユニットテスト、後者は結合テストで使われる事が多い。
RoundTripperの利用
例えば以下の様な何の変哲もないHandlerがあるとする。hello worldを返すだけのもの。
func Handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "hello world"+r.URL.Query().Get("suffix")) }
このHandlerへのrequestに触りたい。とりあえず変更されることがわかれば良いのでquery stringの"suffix"を追加するようなRoundTripperを書いてみる。
// このRoundTripperを利用したい type AddSuffixQuery struct { Transport http.RoundTripper } func (t *AddSuffixQuery) RoundTrip(req *http.Request) (*http.Response, error) { // <url> -> <url>?suffix=!! q := req.URL.Query() q.Add("suffix", "!!") req.URL.RawQuery = q.Encode() tranport := t.Transport if tranport == nil { tranport = http.DefaultTransport } return tranport.RoundTrip(req) }
コレを使ったServerのテストはそのまますんなり書き下せる。hello worldではなくhello world!!が返るかどうかを確認すれば良い。
// TestUseTestServer テストサーバーを使う場合は本物の通信を行う。Transportを見てくれる。 func TestUseTestServer(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(Handler)) defer ts.Close() client := &http.Client{Transport: &AddSuffixQuery{}} res, err := client.Get(ts.URL) if err != nil { t.Fatalf("res: %+v", err) } if want, got := http.StatusOK, res.StatusCode; want != got { t.Errorf("status code\nwant\n\t%d\nbut\n\t%d", want, got) } got, _ := ioutil.ReadAll(res.Body) if want := "hello world!!"; want != string(got) { t.Errorf("body\nwant\n\t%+v\nbut\n\t%+v", want, string(got)) } }
レスポンスのstatusとbodyを確認しているだけのコード。OK。
PASS ok m/example_go5/02record 0.010s
Recorderの利用
通常Recorderを利用したコードは以下の様な形になる。このコードは先程定義したRoundTripperを利用していない。 これを利用する形に書き換えたい。
// TestUseRecord 通常のhttptest.Recorderを使う例 func TestUseRecord(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest("", "http:", nil) Handler(rec, req) res := rec.Result() if want, got := http.StatusOK, res.StatusCode; want != got { t.Errorf("status code\nwant\n\t%d\nbut\n\t%d", want, got) } got, _ := ioutil.ReadAll(res.Body) if want := "hello world"; want != string(got) { t.Errorf("body\nwant\n\t%+v\nbut\n\t%+v", want, string(got)) } }
呼び出すだけなのでコレでおしまい。関数を直接呼び出すのでrequestのContextに詰め込んだ値がそのままHandlerで使えたりと便利なことがあったりする。今回も利用したかったRoundTripperの効果を得るだけ(query stringの追加だけ)なら直接渡すrequestに変更を加えれば良いだけなので、不要かもしれないが、この記事の本題としてはRoundTripperの利用を強制したいということだった。
実はRoundTripperをネストした構造にすることは概ねmiddlewareのように機能する。そして最も下層の処理はnet/http.Transport
の実装ということになる。これをHandlerを呼ぶものに差し替えてあげれば良い。そんなわけで以下のようなRoundTripperのHandlerTripperを定義してあげて使ってあげれば良い1。
type HandlerTripper struct { Rec *httptest.ResponseRecorder Handler http.HandlerFunc } func (t *HandlerTripper) RoundTrip(req *http.Request) (*http.Response, error) { t.Handler(t.Rec, req) return t.Rec.Result(), nil } // TestUseRecordWithTransport そういうTransportを書いてあげれば済む話だ func TestUseRecordWithTransport(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest("", "http:", nil) transport := &AddSuffixQuery{ Transport: &HandlerTripper{Rec: rec, Handler: Handler}, } res, err := transport.RoundTrip(req) if err != nil { t.Fatalf("res: %+v", err) } if want, got := http.StatusOK, res.StatusCode; want != got { t.Errorf("status code\nwant\n\t%d\nbut\n\t%d", want, got) } got, _ := ioutil.ReadAll(res.Body) if want := "hello world!!"; want != string(got) { t.Errorf("body\nwant\n\t%+v\nbut\n\t%+v", want, string(got)) } }
テストも通る。
$ go test PASS ok m/example_go5/02record 0.013s
RoundTripperを使えると何が嬉しいの?
直近での用途を考えてみると、例えばweb APIの呼び出しのテストに失敗したときだけ、あるいはgo test -v
で実行したときだけ、request/responseのtraceを表示するRoundTripperを追加することができたりする2。
まとめ
- RecorderでもServerでもRoundTripperを使える
- こういう一手間のためのライブラリが欲しいかも
gist
-
直接Recorderを渡す必要もなくて、下層のRoundTripperの中で作って使う形にしても良い。実は以前の記事でその様な実装を書いていた。https://pod.hatenablog.com/entry/2020/08/28/212325。↩
-
こういう失敗したときだけの出力を作るときに
t.Logf()
を使って書き出すのが便利。↩