My External Storage

May 29, 2020 - 5 minute read - Comments - go

Goのtestを理解する - httptestサブパッケージ編

Goのテストについていくつかまとめを書いていた。

触れるのを忘れていたhttptestパッケージについてまとめる。

TL;DR

net/http/httptestパッケージ

net/http/httptestパッケージはHTTPのテストを書くのに便利なパッケージだ。 サーバのHTTPハンドラーを書くときは次の2つを利用してテストを書く。

クライアント側の動作確認をしたくて、テストコードでダミーのサーバを立ち上げたいときは次のいずれかを使う。

サーバのHTTPハンドラーのテストコードを書く

Goでwebアプリケーションを書くとき、WebフレームワークやgRPCなどを利用していない場合は、http.HandleFunc型のシグネチャでハンドラー関数を書くだろう。

h := func(w http.ResponseWriter, r *http.Request) {
  io.WriteString(w, "Hello from handler!\n")
}

http.HandleFunc("/", h)
log.Fatal(http.ListenAndServe(":8080", nil))

このようなハンドラーのテストを書くときにわざわざHTTPサーバを立てる、というのは少々めんどくさい。 こんなときに利用できるのがhttptest.NewRequesthttptest.ResponseRecorderだ。

httptest.NewRequestは擬似的なHTTPのリクエストを作成できる。生成関数以外は通常のhttp.Requestと同じ方法でHTTPヘッダーなどを組み立てられる。
httptest.NewRecorderhttp.ResponseWriterを満たす*httptest.ResponseRecorderオブジェクトを取得できる。 このオブジェクトを利用してHTTPハンドラーの戻り値を検証するテストコードを書ける。

具体的には利用するときは次のようなテストコードになるだろう。

// テスト対象のHTTPハンドラー
func myHandler(w http.ResponseWriter, r *http.Request) {
  io.WriteString(w, "Hello from handler!\n")
}

func TestMyHandler(t *testing.T) {
  // Arrange
  reqBody := bytes.NewBufferString("request body")
  req := httptest.NewRequest(http.MethodGet, "http://dummy.url.com/user", reqBody)

  // 生成後は*http.Requestオブジェクトと同じように扱える
  q := req.URL.Query()
  q.Add("", tt.args.code)
  req.URL.RawQuery = q.Encode()

  // レスポンスを受け止める*httptest.ResponseRecorder
  got := httptest.NewRecorder()

  // Act
  myHandler(got, req)
  
  // Assertion
  // http.Clientなどで受け取ったhttp.Responseを検証するときとほぼ変わらない
  if got.Code != http.StatusOk {
      t.Errorf("want %d, but %d", tt.wantStatus, got.Code)
  }
  // Bodyは*bytes.Buffer型なので文字列の比較は少しラク
  if got := got.Body.String(); got != tt.wantBody {
    t.Errorf("want %s, but %s", tt.wantBody, got)
  }
  // http.Responseオブジェクトとしても比較できる。
  if resp := got.Result().Cookies(); resp.ContentLength == 0 {
    t.Errorf("resp.ContentLength was 0")
  }
}

クライアントのテストコードを書くとき

HTTPクライアント側のテストを書くときに便利なのが、*httptest.Serverオブジェクトだ。 実際のHTTP通信を使ったHTTPクライアント側のテストコードを書こうと思うと、次のようなテストコードを書く必要がある。

  1. 通信先のサーバを模すダミーハンドラーを用意する
  2. ダミハンドラーでリッスンする*http.Serverをgoroutineで起動する
  3. 別goroutine上でサーバがリッスンを開始するまで待機する
    • 起動前に通信をしてしまうとテストが失敗する
  4. テスト対象のHTTPクライアントを初期化して、実行する
  5. HTTPクライアントの実行結果を検証する
  6. テスト終了時にダミハンドラーでリッスンするサーバを適切に週有量する

別goroutine上でダミーのサーバを動かして適切に待機、終了させるコードを毎回書くのは面倒だ。
ここで、*httptest.Serverオブジェクトを利用する。このオブジェクトが便利なのは内部でgoroutineを起動してリッスンを開始してくれる点だ。 また、NewServer関数から制御が戻ってきた時点でサーバがリッスンを開始しているため、クライアントが通信を開始するタイミングをあわせる必要もない。

*httptest.ServerオブジェクトはHTTP通信なのか、HTTPS通信によって初期化関数を使い分ける。

コレを使ってHTTPクライアントのテストを書くと、goroutineを意識せずにシンプルなテストコードを書くことができる。

func TestClient(t *testing.T) {
  // Arrange
  // ServeMuxオブジェクトなどを用意してルーティングしてもよい
  h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello, client")
  })
  // 別goroutine上でリッスンが開始される
  ts := httptest.NewServer(h)
  defer ts.Close()

  cli := &http.Client{}
  req, err := http.NewRequestWithContext(context.TODO(), "GET", ts.URL, strings.NewReader(""))
  if err != nil {
    t.Errorf("NewRequest failed: %v", err)
  }

  // Act
  resp, err := cli.Do(req)
  if err != nil {
    t.Fatal(err)
  }

  // Assertion
  got, err := ioutil.ReadAll(resp.Body)
  if err != nil {
    t.Fatal(err)
  }
  resp.Body.Close()
  want := "Hello, client\n"
  if string(got) != want {
      t.Errorf("want %q, but %q", want, got)
  }
}

待ち合わせのコードやgoroutineを起動するようなコードも書かず、シンプルなHTTPクライアントのテストを書くことができた。

終わりに

httptestパッケージを使うと、サーバのコードを書くときサーバ本体の実装を待たずともハンドラーごとのテストを書けて非常に便利だ。
また、ダミーサーバはクライアントの実装だけでなく、サードパーティへの通信を含んだ処理のエンドポイントテストを書くときにも利用できる。
テストの書きやすさはTDDやエンドポイントテストをやるためのモチベーションにもつながるので、このようなテストフレームワークが標準pkgとして公開されているのはとてもありがたい。

参考

その他のテスト関連の記事

関連記事