at kaneshin

Free space for me.

golang の httptest パッケージを使う

この記事は Go Advent Calendar 2016 - Qiita の2日目の記事です。

Golang については書きたいことがたくさんあるので、Go Advent Calendar 2016 その4が出てきても良いのではと思っている次第です。(空いていればいつでも書きます)

さて、今回、この記事では Golang で書かれた Web アプリケーションのリクエストのユニットテストについて解説しようと思います。

github.com

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 パッケージは軽量ですが、テスト時に強力なパッケージになりますので、是非活用してもらえればと思います。

リソース