チームの共用語(?)がGoになったというのもあってGoのテストについて入門し始めた。テストコード自体はlinux-test-project/ltpを独自でforkしたものをメンテナンスしていたりとCではあるが基礎ぐらいは取得してるかなという感じでした。入門していく中でMockについて学ぶ機会があってこれまであまり触れる機会がないものだったので理解に苦しんだが少しわかったので書いてみる。
そもそもMockに慣れてないのはなぜだろうと考えたら
割とこの記事にあるような環境にいたからなのかなと思ったりした。Mock使うぐらいなら関数に切り出してテストしやすくしたり単体テストで通らないパスはintegrationとかに自動テストを実装しておいてMock不要にしてテストしたりそんな感じでことなきを得てきた(?)。あとはMockを使ってテストではエビデンスとしては足りなくて次工程に回せないなどの特殊な理由もあって個人の開発のしやすさ的に使ってる人はいたかもだけどあまり積極的に書いても出荷判定という壁を越える材料にはならないので使われないみたいなのもあったのかなとか思っている。(これは特殊すぎる例かもしれない...)
外部APIっぽいものを書く
テストしにくければなんでもよくて例えばhttpアクセスがきたら100までの数字のうちランダムで返すものを用意しておく
package main import ( "fmt" "math/rand" "net/http" "strconv" "time" ) func handler(w http.ResponseWriter, r *http.Request) { rand.Seed(time.Now().UnixNano()) fmt.Fprintf(w, strconv.Itoa(rand.Intn(100))) } func main() { http.HandleFunc("/", handler) http.ListenAndServe(":8080", nil) }
テスト対象のコードを書く
上記のAPIを使ったコードを書くとこんな感じ。テスト対象はRunになるがRunの戻りの文字列はAPIの結果次第で変わってしまうという実装になっている。これを単体テストするとなると取りうる数値を許容するテストを書くかDoHttpRequestで固定値を返すようなオプションを実装するかになってしまう(これまでなら多分そうしてた)。こういう時に使えるのがモックを使ったテストになる。
package main import ( "fmt" "io/ioutil" "net/http" ) func DoHttpRequest(url string) string { resp, _ := http.Get(url) defer resp.Body.Close() byteArray, _ := ioutil.ReadAll(resp.Body) return string(byteArray) } func Run(s string) string { return s + DoHttpRequest("http://localhost:8080") // 引数の文字列と乱数を連結して返す } func main() { fmt.Println(Run("test: ")) }
mockを使うには
動作を分けたい対象をinterfaceを使って抽象化してテストコードでは対象のinterfaceにテスト用の実装(Mock)をDIするという感じになる。今回分けたい対象はDoHttpRequestなのでこれを使っていくとこんな感じ。interfaceを定義する。
package main import ( "fmt" "io/ioutil" "net/http" ) type APIer interface { DoHttpRequest(url string) string } type API struct {} func (a API) DoHttpRequest(url string) string { resp, _ := http.Get(url) defer resp.Body.Close() byteArray, _ := ioutil.ReadAll(resp.Body) return string(byteArray) } func Run(s string, a APIer) string { return s + a.DoHttpRequest("http://localhost:8080") } func main() { a := &API{} fmt.Println(Run("test: ", a)) }
テストコードはこんな感じ。APIMockを定義してinterfaceを実装していくことでmockを使ってRunをテストすることができるようになる。
package main import ( "testing" ) type APIMock struct {} func (a APIMock) DoHttpRequest(url string) string { return "" } func TestRand(t *testing.T) { ai := &APIMock{} tests := []struct { a string b string }{ { a: "test: 1", b: "test: 1", }, } for _, tt := range tests { res := Run(tt.a, ai) if tt.b != res { t.Errorf("got: %s, want: %s", res, tt.b) } } }
interfaceの埋め込み
以下のようにinterfaceが定義されているとする。これをテストしたいのはDoHttpRequestだけの場合にテストコードに全て実装していくのは大変になってしまう。そういう時に使えるのがInterfaceの埋め込み。(これはTDDとかで使えたりするのだろうか)。埋め込み自体はeffective_goとかにも載っていた。
type APIer interface { DoHttpRequest(url string) string DoHttpRequest2(url string) string DoHttpRequest3(url string) string DoHttpRequest4(url string) string DoHttpRequest5(url string) string }
type APIMock struct { APIer }
gomock
他言語にもあるようなツールなのであるだろうなと思ってみてたらあった。毎回作るのも面倒臭いしで便利そう。