今川館

都内勤務の地味OLです

はじめての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を心がけて書いてみた。

テストしやすいGoコードのデザイン

[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にはちゃんと用意されているものだと感心した。