46
40

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 2014

Day 24

net/http/httptestを使ってみよう。

Last updated at Posted at 2014-12-23

Goの標準のHTTPサーバの機能を使ったアプリケーションのテスト

Goの標準のHTTPサーバの機能を使ったアプリケーションのテストをするにはどうするか。というテーマが少なそうだったので、ちょっと調べてみました。

##サンプルコードその1
サーバ側のコードもテストコードもダミーなのですが、標準的に使うには以下のようにするようです。(Goのnet/httpパッケージにも実際に使われています)

ポイントは、httptest.NewServer([ハンドラ])を呼び出すところです。

後は、普通にリクエストを送信して下さい。

sample1_test.go(その1)
// 本来の処理のダミーその1
var sampleHandler = http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello HTTP Test")
})

func TestNormal(t *testing.T) {
	ts := httptest.NewServer(sampleHandler)
	defer ts.Close()

	// リクエストの送信先はテストサーバのURLへ。
	r, err := http.Get(ts.URL)
	if err != nil {
		t.Fatalf("Error by http.Get(). %v", err)
	}

	data, err := ioutil.ReadAll(r.Body)
	if err != nil {
		t.Fatalf("Error by ioutil.ReadAll(). %v", err)
	}

	if "Hello HTTP Test" != string(data) {
		t.Fatalf("Data Error. %v", string(data))
	}
}

##サンプルコードその2
httpsのテストがしたい場合はhttptest.NewTLSServer()に変えます。実際にts.URLの値は、httpsになっていますので、Clientのオブジェクトも標準のものではなく、TLSClientConfigを設定する必要があります。

sample1_test.go(その2)
func TestTLS(t *testing.T) {
	ts := httptest.NewTLSServer(sampleHandler)

	tr := &http.Transport{
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true,
		},
	}

	c := &http.Client{Transport: tr}

	r, err := c.Get(ts.URL)
	if err != nil {
		t.Fatalf("Error by http.Get(). %v", err)
	}

	data, err := ioutil.ReadAll(r.Body)
	if err != nil {
		t.Fatalf("Error by ioutil.ReadAll(). %v", err)
	}

	if "Hello HTTP Test" != string(data) {
		t.Fatalf("Data Error. %v", string(data))
	}
}

##サンプルコードその3
例えば、「User-AgentがAndroid以外の場合は404を返す。」と言った感じのテストです。
(本当は、もっとたくさんのUser-Agentをテストするのかもしれませんが…)

sample1_test.go(その3)
// 本来の処理のダミーその2
var sample2Handler = http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
	ua := r.Header.Get("User-Agent")

	if !strings.Contains(ua, "Android") {
		http.Error(w, "Not Found.", http.StatusNotFound)
		return
	}

	fmt.Fprintf(w, "Hello HTTP Test")
})

func TestAndroid(t *testing.T) {
	var requests [2]*http.Request
	var err error

	ts := httptest.NewServer(sample2Handler)
	defer ts.Close()

	requests[0], err = http.NewRequest("GET", ts.URL, nil)
	if err != nil {
		t.Errorf("NewRequest[0] Error. %v", err)
	}
	requests[0].Header.Add("User-Agent", "Mozilla/5.0 (iPad; CPU OS 8_1_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/39.0.2171.50 Mobile/12B440 Safari/600.1.4")

	requests[1], err = http.NewRequest("GET", ts.URL, nil)
	if err != nil {
		t.Errorf("NewRequest[1] Error. %v", err)
	}
	requests[1].Header.Add("User-Agent", "Mozilla/5.0 (Linux; Android 4.4.2; 302KC Build/101.0.2c00) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36")

	c := http.DefaultClient

	for pos, req := range requests {
		r, err := c.Do(req)
		if err != nil {
			t.Fatalf("Error by http.Get(). %v", err)
		}

		data, err := ioutil.ReadAll(r.Body)
		if err != nil {
			t.Fatalf("Error by ioutil.ReadAll(). %v", err)
		}

		if r.StatusCode == 200 {
			if "Hello HTTP Test" != string(data) {
				t.Fatalf("Data Error. %v", string(data))
			}
		} else {
			if r.StatusCode != 404 {
				t.Fatalf("Status Error %d", r.StatusCode)
			}

			if "Not Found.\n" != string(data) {
				t.Fatalf("Data Error. %v", string(data))
			}

			if pos != 1 {
				t.Fatalf("Request Error. %d", pos)
			}
		}
	}

}

### 一つ問題が。
書き方がおかしいのか、ハンドラは間違いなく、404を返している(http.Error()を呼び出している)にもかかわらず、なぜか、レスポンスが全て200になる現象がでました。これでは、正常系の確認ができても異常系のテストが正しくできないということになるので、少し不便です。(理由がよくわからなかったので、すでに使っている方は教えて下さい)

2014/12/28追記:
サンプルコードのハンドラの呼び出しがおかしいのと、bodyのテストが間違っていたので修正しました。ちゃんと404が返ってきました。
GDG Cafe #1beta2でご指摘いただきました。ありがとうございました。

本題に戻って、これはGAE/Goには使いづらい(?)

GAE/Goでもnet/http/httptestを使ってみようと思うと、上の書き方はちょっと使えなさそうな予感がする。ので、GAE/Go、普通のGoの両方で使える、リクエストとレスポンスを自作する方法を紹介しておきます。

サンプルコード(その4)

sample1_test.go
func TestAndroidNoServer(t *testing.T) {
	var requests [2]*http.Request
	var err error

	requests[0], err = http.NewRequest("GET", "/hoge", nil)
	if err != nil {
		t.Errorf("NewRequest[0] Error. %v", err)
	}
	requests[0].Header.Add("User-Agent", "Mozilla/5.0 (iPad; CPU OS 8_1_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/39.0.2171.50 Mobile/12B440 Safari/600.1.4")

	requests[1], err = http.NewRequest("GET", "/hoge", nil)
	if err != nil {
		t.Errorf("NewRequest[1] Error. %v", err)
	}
	requests[1].Header.Add("User-Agent", "Mozilla/5.0 (Linux; Android 4.4.2; 302KC Build/101.0.2c00) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36")

	for pos, req := range requests {
		r := httptest.NewRecorder()

		sample2Handler(r, req)

		data, err := ioutil.ReadAll(r.Body)
		if err != nil {
			t.Fatalf("Error by ioutil.ReadAll(). %v", err)
		}

		if r.Code == 200 {
			if "Hello HTTP Test" != string(data) {
				t.Fatalf("Data Error. %v", string(data))
			}
		} else {
			if r.Code != 404 {
				t.Fatalf("Status Error %d", r.Code)
			}

			// httptest.ResponseRecorderは改行コードが付いてしまう
			if "Not Found.\n" != string(data) {
				t.Fatalf("Data Error. %v", string(data))
			}

			if pos != 0 {
				t.Fatalf("Request Error. %d", pos)
			}
		}
	}
}

ソースコード全体

ソースコードはgithubに公開してありますのでそちらをごらんください。
https://github.com/tyokoyama/golangcafe/tree/master/httptestsample

結論

  • もしHTTPのテストでローカルサーバを起動する処理(ListenAndServe()を呼び出す)を書かれている方はこちらの方法に書きなおした方が良いと思います。
  • 実際に導入してみないとわからないので、導入してみてくださいw

Disclaimer

  • この記事は個人的なものです。私の雇用者とは全く関係はありません。(一応つけておきます)
46
40
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
46
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?