GoによるHTTP呼び出しのユニットテスト
はじめに
外部のWebサービスから、HTTP(HTTPS)で情報を取得し、その結果を利用するというのはよく行われます。 Goの標準パッケージにはHTTPクライアントが含まれているため、その処理自体を書くのは簡単です。 しかしこれのテストを行うとなると少し難しくなります。 この記事ではHacker News API(https://github.com/HackerNews/API)を例として、どのようにHTTP呼び出しのテストを書くのか紹介します。
HTTP呼び出しの例
Hacker News APIのうち、トップストーリーのID一覧を取得するAPIを呼び出す関数を書いてみます。 このトップストーリーAPIは、整数で表現された、ストーリーのidを配列で返します。 具体的には以下のようになります(標準パッケージのみを使用しているためimport省略)。
package main var HNApiBaseURL = "https://hacker-news.firebaseio.com/v0" func GetHNTopStories() ([]int, error) { res, err := http.Get(HNApiBaseURL + "/topstories.json") if err != nil { return nil, err } defer res.Body.Close() ids := []int{} if err := json.NewDecoder(res.Body).Decode(&ids); err != nil { return nil, err } return ids, nil } func main() { ids, _ := GetHNTopStories() fmt.Print(ids) }
単純なGETのAPIでもあり、特に難しいところはありません。ポイントとしてはHacker News APIのURLのうち、ドメイン部分など共通の部分をvarで定義しているところです。
テストコード
問題の確認
上記のコードをテストする上で問題となるのは、http.DefaultClient.Do(req)
で実際にHTTPリクエストするところです。
何も考えずに、GetHNTopStories
のテストを書いてしまうと、実際にHacker NewsのAPIを実際に叩くことになります。
これは相手に迷惑ですし、外に接続できない環境ではテスト出来なくなります。
解決策の検討
私たちはトップストーリーのAPIがintの配列を返すことを知っています。つまりこのAPIをエミュレートするMockとしてのhttp.Handlerを書いて、それをローカルで動かし、そこに対してリクエストを送るようにしてやれば解決しそうです。
Mockの作成
まずはこのAPIをエミュレートするMockHandlerを書いてみます。
type mockHNStoriesAPI struct { Result []int Err error } func (mock *mockHNStoriesAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) { if mock.Err != nil { panic(mock.Err) } json.NewEncoder(w).Encode(mock.Result) }
mockHNStoriesAPI
は2つのフィールドを持ちます。
Resultは正常系の場合にこのHandlerがレスポンスに設定するためのintスライスです。トップストーリーAPIが返すidの配列を設定しておくためのものです。
Errはエラー状態を作り出すためのものです。ここで言うエラーは500 Internal Server Errorなどではなく、そもそもサーバーにつながらないなどの状態を示します。
そして、mockHNStoriesAPI
をHandlerとして機能させるためServeHTTP
を実装します。APIのドキュメントから、細かい仕様は読み取れないため、レスポンスがある場合は200 OKであるという想定です。
テストサーバーの作成
Mockが出来上がったので次はテストサーバーを作成します。
Goの標準パッケージにはnet/http/httptest
というものがあり、このパッケージはテスト用のサーバーを作成するためのhttptest.NewServer
関数を提供しています。この関数はHandlerを引数として、テスト用サーバー(*httptest.Server)を生成します。
すでにmockHNStoriesAPI
はHandlerなので、自身を引数としてテストサーバーを返すメソッドを作るだけです。
func (mock *mockHNStoriesAPI) newTestServer() *httptest.Server { return httptest.NewServer(mock) }
ユニットテストを書く
あとは mockHNStoriesAPI
を使ってユニットテストを書くだけです。
newTestServer()
で作ったテストサーバーのURLで、HNApiBaseURLを上書きすれば、GetHNTopStoriesはテストサーバーに接続するようになります。
func Test_GetHNTopStories(t *testing.T) { mock := &mockHNStoriesAPI{ Result: []int{1, 2, 3}, } ts := mock.newTestServer() HNApiBaseURL = ts.URL ids, err := GetHNTopStories() if err != nil { t.Error(err) } if len(ids) != 3 { // 実際にはもう少しマシなassertを書くべきだろう、、、 t.Error("expect 3") } }
補足
このテストコードのポイントは、通常の実行時とテストの実行時で接続先を切り替えているところです。 サンプルコードではグローバルな変数を書きかえることで実現しましたが、実際のプロダクトでは環境変数や設定ファイルなどで切り替えを行い、実行中に上書きされないようにするべきでしょう。
今回は単純なAPIを対象としましたが、現実にはもっと複雑なAPIを使うことも多いです。そのようなAPIのMockを作っていると、Mockそのものが複雑化していくことがままあります。しかしテストのためのMockが複雑化するのはよいことではありません。場合に応じて1つのAPIに対して複数のMockを作ったり、整理のためにMock用のテストパッケージを作ることも考えるべきでしょう。