Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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の一手法を紹介しました。
なんでも適用すると思わぬところでテストが機能しなくなることがありますが、有用な手段な場合もあると知っておくとユニットテストが書きやすくなります。

speee
株式会社Speeeは「解き尽くす。未来を引きよせる。」というミッションを実現すべく、中長期的な目線で企業価値を最大化させていくため、組織・事業のStyleを大切にした永続的な価値創造を目指しています。
https://www.speee.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした