はじめてのwebアプリ⑤ ユニットテストを追加
ユニットテストを追加する頃合いに
文字列とファイルに対応した文字数カウント機能を作ったところで、これらのユニットテストを作る頃合いになってきた。
また、リクエストを受け取ってレスポンスに書き込むハンドラ関数のテストも欲しいところだ。
そこで、ユニットテストを追加する工程をここに記していきたい。
まず、今までmain.goに全ての処理を記述していたプログラムを、以下の構成に変更した。
kanjo ├── lib │ └── wc │ ├── main.go │ └── main_test.go ├── main.go └── main_test.go
lib/wcの配下に文字数カウントモジュールを移動し、lib/wc/main_test.goで文字数カウントのテストを、main_test.goでハンドラ関数のテストを書いていく。
文字数カウントのプログラム
アプリケーションコード
まずlib/wcに移動したプログラムは以下、
[lib/wc/main.go]
func WordCountInString(text string) (count, byte_count, invalid int) { b := []byte(text) for len(b) > 0 { r, size := utf8.DecodeRune(b) if r == utf8.RuneError { invalid += size } else { count++ byte_count += size } b = b[size:] } return } // ファイルを読み取って文字数、バイト数、不正バイト数を数えて返す。 func WordCountInFile(rd io.Reader) (count, byte_count, invalid int) { in := bufio.NewReader(rd) for { r, size, err := in.ReadRune() if err == io.EOF { break } if r == utf8.RuneError { invalid += size } else { byte_count += size // 正常な文字だけカウントする。 count++ } } return }
関数をふたつ定義し、それぞれ文字列とファイルを引数に取る。
(カウントの処理を更に共通化したらどうか?と思う人もいるだろうが、今回は見送り)
テストコード
テストコードは以下のスライドの記述を参考にTable Driven Testを心がけて書いてみた。
[lib/wc/main_test.go]
package wc // テストを実行するとき // $ go test github.com/oyakata/kanjo/lib/wc/ import ( "strings" "fmt" "testing" ) type T struct { Input string Count, ByteCount, Invalid int } var valid, broken, exp string func init() { valid = string([]byte{ 240, 169, 184, 189, // 𩸽 227, 129, 174, // の 227, 129, 178, // ひ 227, 130, 137, // ら 227, 129, 141, // き 240, 159, 153, 134, // face with ok gesture 240, 159, 153, 134, // face with ok gesture 240, 159, 153, 134, // face with ok gesture }) broken = string([]byte{ 169, 184, 189, // [NG] 先頭欠損 227, 129, 174, // の 129, 178, // [NG] 先頭欠損 227, 137, // [NG] 2桁目欠損 227, 129, // [NG] 3桁目欠損 240, 153, 134, // [NG] 2桁目欠損 240, 159, 134, // [NG] 3桁目欠損 240, 159, 153, // [NG] 末尾欠損 }) part := "𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。" // 46 * 3 + 7 + 2 = 147文字 // 1行: (4byte * 2文字) + (3byte * 43文字) + (1byte * 1文字) = 138byte // 全体: 140 * 3 + 7 + 6 = 427byte exp = fmt.Sprintf("%v\n\n%v\n\n\n%v\n\n以上", part, part, part) } func TestWordCountInString(t *testing.T) { cases := []T{ {valid, 8, 28, 0}, {broken, 1, 3, 18}, {"", 0, 0, 0}, {exp, 147, 427, 0}, } for _, tc := range cases { x, y, z := WordCountInString(tc.Input) if x != tc.Count || y != tc.ByteCount || z != tc.Invalid { t.Errorf("WordCountInString=%v% v% v, want=%v% v% v", x, y, z, tc.Count, tc.ByteCount, tc.Invalid, ) } } } func TestWordCountInFile(t *testing.T) { cases := []T{ {valid, 8, 28, 0}, {broken, 1, 3, 18}, {"", 0, 0, 0}, {exp, 147, 427, 0}, } for _, tc := range cases { x, y, z := WordCountInFile(strings.NewReader(tc.Input)) if x != tc.Count || y != tc.ByteCount || z != tc.Invalid { t.Errorf("WordCountInString=%v% v% v, want=%v% v% v", x, y, z, tc.Count, tc.ByteCount, tc.Invalid, ) } } }
引数が文字列とファイルで違うけれども、基本的に確かめたいテストデータの内容は同じなのでinit()に共通化してしまったのだが個人的にはどうもしっくりこない。共通化したつもりがかえって読みづらくなった感がある。が、経験不足ということでとりあえずこれで良しとした。(テストもOKを確認)
ハンドラ関数のプログラム
アプリケーションコード
とりあえず、JSONで結果を返すハンドラ関数のテストだけ書いてみた。
まず、ハンドラ関数のコードは以下。
[main.go]
package main // import "encoding/json" // import "github.com/oyakata/kanjo/lib/wc" // import "net/http" // json.Marshalは構造体の公開フィールドしか出力してくれないので注意。 // 小文字でJSONのキーを出力したければタグを指定する。 type WordCount struct { Text string `json:"text"` Count int `json:"count"` ByteCount int `json:"byte_count"` } func JSONWordCountHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") text := r.FormValue("text") count, bc, _ := wc.WordCountInString(text) result, err := json.Marshal(WordCount{text, count, bc}) if err != nil { log.Panic(err) } w.Write(result) }
なお、テストコードの方でUnmarshalしたいのでWordCountの構造体をトップレベルに移動した。
テストコード
問題は、ハンドラ関数をテストするときに引数に渡すhttp.ResponseWriterとhttp.Requestの作り方なのだが…
まずコードを示すと以下の通り。
[main_test.go]
package main import ( "fmt" "encoding/json" "net/http" "net/http/httptest" "testing" ) func TestJSONWOrdCountHandler(t *testing.T) { v := "今川義元1560年桶狭間" location := fmt.Sprintf("http://example.com/count?format=json&text=%v", v) w := httptest.NewRecorder() // httptestにはなぜかNewRequestはないと言われる(go version 1.6.2) // undefined: httptest.NewRequest r, _ := http.NewRequest("GET", location, nil) JSONWordCountHandler(w, r) if w.Code != http.StatusOK { t.Errorf("JSONWordCountHandler=%v, got=%v", http.StatusOK, w.Code) } result := &WordCount{} json.Unmarshal([]byte(w.Body.String()), result) expected := WordCount{v, 12, 28} if *result != expected { t.Errorf("JSONWordCountHandler=%v, got=%v", expected, result) } }
net/http/httptestという便利モジュールが存在する
ハンドラ関数の第1引数、http.ResponseWriterのオブジェクトをどうやって作ったら良いか最初わからず調べるのに結構苦労した。
何と、net/http/httptestというテスト向けのモジュールがちゃんと用意されていた。
httptest.NewRecorder()がResponseWriterインターフェースを実装したオブジェクトを返してくれるのでこれを使う。
リクエストオブジェクトを作るときはnet/http.NewRequst
Goのドキュメントを読むと、httptestにNewRequestが定義されていると書いてあるのだが、自分の手元で試すとundefinedと言われるのでnet/httpのNewRequestを使って対処した。
httptest - The Go Programming Language
よって、ハンドラ関数の引数が無事に作成できた。
w := httptest.NewRecorder() // httptestにはなぜかNewRequestはないと言われる(go version 1.6.2) // undefined: httptest.NewRequest r, _ := http.NewRequest("GET", location, nil) JSONWordCountHandler(w, r)
その気になればユニットテストを書く仕組みはGoにはちゃんと用意されているものだと感心した。