この記事は Go Advent Calendar 2016 - Qiita の2日目の記事です。
Golang については書きたいことがたくさんあるので、Go Advent Calendar 2016 その4が出てきても良いのではと思っている次第です。(空いていればいつでも書きます)
さて、今回、この記事では Golang で書かれた Web アプリケーションのリクエストのユニットテストについて解説しようと思います。
1. Testing HTTP Handler
検証のために、ただ単に "pong" を返却する pingHandler と、URLクエリから値を取得してそのまま返却する echoHandler の2つを定義します。
ー pingHandler
// pingHandler returns just "pong" string. func pingHandler() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("pong")) } }
ー echoHandler
// echoHandler returns a 'msg' query parameter string. func echoHandler() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(r.URL.Query().Get("msg"))) } }
※ ヘッダーやステータスコードの設定は省略しています。
Tests with httptest.Server
本テストコードは こちら です。
実装したHTTPハンドラのテストを実行するために、httptest (net/http/httptest) パッケージを使用してリクエストをサービングします。
ー pingHandler テスト
t.Run("pingHandler", func(t *testing.T) { t.Parallel() s := httptest.NewServer(http.HandlerFunc(pingHandler())) defer s.Close() res, err := http.Get(s.URL) assert.NoError(t, err) defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) assert.NoError(t, err) assert.Equal(t, "pong", string(body)) })
ー echoHandler テスト
t.Run("echoHandler", func(t *testing.T) { candidates := []struct { query string expected string }{ {"", ""}, {"foo=bar", ""}, {"msg=foo", "foo"}, } for _, c := range candidates { c := c t.Run(c.query, func(t *testing.T) { t.Parallel() s := httptest.NewServer(http.HandlerFunc(echoHandler())) defer s.Close() res, err := http.Get(fmt.Sprintf("%v?%v", s.URL, c.query)) assert.NoError(t, err) defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) assert.NoError(t, err) assert.Equal(t, c.expected, string(body)) }) } })
テストのアサートには github.com/stretchr/testify/assert を使用しており、テストの記述については go1.7 以上の仕様となりますので、 Golang におけるサブテストの並行処理実装について | eureka tech blog を是非一読してみてください。
httptest.NewServer とは
上の例のように、既にHTTPハンドラが定義されている場合、 httptest.Server を利用してリクエストをサービングします。
httptest.NewServer(http.Handler) *httptest.Server
httptest.NewServer を呼んで httptest.Server を初期化します。httptest.Server は httptest.NewServer で初期化を行うと、リクエストをサーブする状態の httptest.Server が返却されるため、使用後に Close を行う必要があります。
また、そもそも初期化時にサービングしている httptest.Server が欲しくない場合は
httptest.NewUnstartedServer(http.Handler) *httptest.Server
にて初期化を行います。
httptest.NewServer の使い方
// http.HandleFuncを満たすハンドラの用意 handler := http.HandleFunc(func(w http.ResponseWriter, r *http.Request) { // ... }) // 定義したハンドラをサーブする Server s := httptest.NewServer(handler) // s.URL にリクエストURLが設定されている res, err := http.Get(s.URL) // .... // 適切に Close させて終了 s.Close()
httptest.NewServer には http.Handler を満たした変数を渡せば十分です。 http.Handler を渡すこすためには func(w http.ResponseWriter, r *http.Request) のハンドラ関数を http.HandleFunc 型にするだけで満たされます。
handler := http.HandleFunc(func(w http.ResponseWriter, r *http.Request) { ... })
2. Recording Response while Testing
httptest.Server を使用するのは上記で十分ですが、わざわざ検証を行うために httptest.Server を使って検証を行いたくない場合や、何らかの理由で httptest.Server が使用できないが、リクエストをシミュレートしてのユニットテストを行いたい場合は httptest.ResponseRecorder を使用します。
httptest.ResponseRecorder
Responseを記録するための構造体で、httptest (net/http/httptest) にこちらも存在しています。
type ResponseRecorder struct { Code int // the HTTP response code from WriteHeader HeaderMap http.Header // the HTTP response headers Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to Flushed bool // contains filtered or unexported fields }
http.ResponseWriter を満たしているため、HTTPハンドラに渡すことによって結果を書き込むことができます。
Tests with httptest.ResponseRecorder
本テストコードは こちら です。
HTTPハンドラに ResponseRecorder を渡して、その値を検証することになります。
// ResponseRecorder の生成 res := httptest.NewRecorder() // リクエストの生成 req, err := http.NewRequest(http.MethodGet, "http://example.com/?msg=foo", nil)
用意したリクエストとレスポンスに対してHTTPハンドルします。
// ハンドラに ResponseRecorder と Request を渡して実行する
handler := echoHandler()
handler(res, req)
※ echoHandler は Server で使用したものと同一
// ResponseRecorder に記録された値の検証 assert.Equal(t, "text/plain", res.HeaderMap.Get("Content-Type")) assert.Equal(t, 200, res.Code) // Body の検証 body, err := ioutil.ReadAll(res.Body) assert.NoError(t, err) assert.Equal(t, c.expected, string(body))
検証には testify/assert を使用しています。httptest.ResponseRecorder には Response 検証に必要な情報が全て格納されているので、それを利用して検証を行います。
おわりに
net/http/httptest パッケージは軽量ですが、テスト時に強力なパッケージになりますので、是非活用してもらえればと思います。