18
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GoAdvent Calendar 2015

Day 6

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

Last updated at Posted at 2015-12-05

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の設定が残ったままになることになるので注意するところです。

ライブラリ化

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

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

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

18
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?