post Image
外部APIを使うController用のTest Doubleライブラリを書いた

Goで net/http を使ったControllerのテストには、 net/http/httptest モジュールを使うと擬似的にサーバをその場で立て、登録したハンドラにリクエストを送れます。
ただ、APIに使うアクションはたびたび、外部のAPIを叩くこともあります。
その際、テストを実行するときに外部APIを使えない状況(本番環境とは違う状況、例えば、production用のAPIの発行には金銭が発生するなど)に遭遇します。

Test Double

そんなときに知られているテクニックとして、テスト時に外部APIへのリクエストをDouble(Stub)することです。
その外部APIが返却するであろう期待値を登録しておけば正常ケースを、エラー値を登録しておけばエラーケースをテストできます。
ここで例に挙げるStubする方法は、 http.Client をもとにHTTPリクエストすることを前提としています。

ではどうやってリクエストをStubすれば良いでしょうか?
net/http でのhttp clientでは、 RoundTripper というインタフェースを軸にHTTPコネクションを制御しています。
https://github.com/golang/go/blob/master/src/net/http/client.go#L41

そこで、このRoundTripperをテスト時にすげ替えてあげれば良いわけです。
RoundTripperは、 *http.Request を受け取り *http.Response を返す、RoundTrip関数を充足すれば良いことがインタフェースの定義からわかります。
https://github.com/golang/go/blob/master/src/net/http/client.go#L99

次のコードは http://example.com/api/test へのリクエストをStubするコードです。
レスポンスのbodyがJSONで、ステータスが200であることを想定しています。

type MyTransport struct {}

func (t *MyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    if req.URL.String() == "http://example.com/api/test" {
        resp := &http.Response{
            Header:     make(http.Header),
            Body:       ioutil.NopCloser(strings.NewReader(`{"api":"test"}`)),
            StatusCode: http.StatusOK,
        }
        resp.Header.Set("Content-Type", "application/json")
        return resp, nil
    } else {
        return http.DefaultTransport.RoundTrip(req)
    }
}

func TestApi(t *testing.T) {
    http.DefaultClient.Transport = &MyTransport{}
    defer func() {
        http.DefaultClient.Transport = nil
    }()

    ts := httptest.NewServer(http.HandlerFunc(api))
    defer ts.Close()

    res, err := http.Get(ts.URL)
    if err != nil {
        t.Error("unexpected", err)
    }
    defer res.Body.Close()

        //...
}

ここで重要なのは、テスト対象のURLのみをstubしていることです。
それ以外のURLへのリクエストは、 デフォルトのRoundTrip関数が定義されている DefaultTransport を使います。
その理由として、テスト時に外部APIへのリクエストだけではなく、ローカルのテストサーバへのリクエストもstubされてしまうからです。
また、DefaultClientのTransportもテスト終了後nilで初期化しておかないと後続のテスト時にStubの設定が残ったままになることになるので注意するところです。

ライブラリ化

毎回テスト時にこういったコードを書くのは煩雑になるので、簡単にラップしたものを作りました。

https://github.com/yoppi/gg

使い方は簡単です。先ほどのテストコードが次のようになります。

func TestApi(t *testing.T) {
    double := gg.Double(map[string]*gg.ResponseHandler{
        "http://example.com/api/test": &gg.ResponseHandler{
            HandleFunc:  apiResponseHandler,
            Status:      http.StatusOK,
            ContentType: "application/json",
        },
    })
    defer double.Close()

    ts := httptest.NewServer(http.HandlerFunc(api))
    defer ts.Close()

    res, err := http.Get(ts.URL)
    if err != nil {
        t.Error("unexpected", err)
    }
    defer res.Body.Close()

        // ...
}

URLに対して、処理させたいHandlerとHTTP Status、Content-Typeを登録しておくシンプルな作りです。
複数のURLも登録できるのでわりと汎用的に使えるインタフェースになっているかなと思います。

まとめ

今回は、HTTPリクエストにおけるTest Doubleの一手法を紹介しました。
なんでも適用すると思わぬところでテストが機能しなくなることがありますが、有用な手段な場合もあると知っておくとユニットテストが書きやすくなります。


『 Go 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。