2019年4月21日日曜日

gomock を使ってモックテストを書いてみる

概要

gomock を使ってモックテストを書いてみました
基本的な使い方やポイントを紹介します

環境

  • macOS 10.14.4
  • go 1.11.5

gomock のインストール

  • go get github.com/golang/mock/gomock
  • go install github.com/golang/mock/mockgen

サンプルアプリ

簡単な Web アプリを用意しました
アプリは別に Web アプリでなくても OK です
この中のハンドラからコールされている Hello インタフェースの関数 Echo()を gomock を使って mock してみたいと思います

  • vim $GOPATH/src/github.com/hawksnowlog/a/main.go
package main

import (
    "net/http"
)

type Hello interface {
    Echo() string
}

type HelloST struct {
    Msg string
}

func (st HelloST) Echo() string {
    return st.Msg
}

func HelloHandler(w http.ResponseWriter, r *http.Request) {
    st := HelloST{"HELLO"}
    w.Write([]byte(st.Echo()))
}

func main() {
    http.HandleFunc("/", HelloHandler)
    http.ListenAndServe(":8080", nil)
}

mock を作成する

  • mkdir mock
  • mockgen -source main.go

で標準出力に mock 用のコードが出力されます
問題なければファイルに記載します

  • mockgen -source main.go -destination mock/mock.go

自動で生成されるコードなので mock.go の内容は省略します

mock を使ったテストを書く

生成した mock を使ってテストを書いてみます

  • vim $GOPATH/src/github.com/hawksnowlog/a/main_test.go
package main

import (
    "fmt"
    "github.com/golang/mock/gomock"
    "github.com/hawksnowlog/a/mock"
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestHello(t *testing.T) {
    // mock
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()
    hello := mock_main.NewMockHello(ctrl)
    hello.EXPECT().Echo().Return("HELLO!!")
    // call api
    testserver := httptest.NewServer(http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(hello.Echo()))
    }))
    defer testserver.Close()
    res, err := http.Get(testserver.URL)
    if err != nil {
        t.Error(err)
    }
    b, err := ioutil.ReadAll(res.Body)
    defer res.Body.Close()
    if err != nil {
        t.Error(err)
    }
    fmt.Println(string(b))
    if string(b) != "HELLO!!" {
        t.Error("error")
    }
}
  • go test github.com/hawksnowlog/a

解説

まず Echo() 関数を mock します
Controller を作成しそれを元にインタフェースの mock を作成します
mock を作成したらインタフェースにある関数を更に mock します

ctrl := gomock.NewController(t)
defer ctrl.Finish()
hello := mock_main.NewMockHello(ctrl)
hello.EXPECT().Echo().Return("HELLO!!")

関数を mock する際は EXPECT().関数名().Return() という感じで定義します
今回であれば本来返るべき「HELLO」の代わりに「HELLO!!」を返すように定義します

あとは今回は net/http のテストなので httptest を使ってハンドラを定義します
ハンドラは実際のインタフェースの関数ではなく mock したインタフェースの関数を使うように定義します

これで実際にサーバにリクエストが行っても mock された関数がコールされるようになります

本当は既存のハンドラをコールしてそこで呼び出している関数を mock したい

本当は以下のようにテスト側でハンドラを呼び出して

testserver := httptest.NewServer(http.HandlerFunc(HelloHandler))

main.go のハンドラで呼び出されている st.Echo() の代わりに mock の hello.Echo() を呼び出してほしかったのですができませんでした

func HelloHandler(w http.ResponseWriter, r *http.Request) {
    st := HelloST{"HELLO"}
    w.Write([]byte(st.Echo()))
}

他の言語の話の出すのはどうかと思いますがイメージとしては ruby の rspec-mock のようにアプリ側の呼び出しをそっくりそのまま mock が返すようにしたかった感じです
今回の場合だとハンドラを結局再定義しているので、そこが微妙な点かなと思います

最後に

gomock を使ってモックテストを書いてみました
とりあえず使い方は理解できたかなと思います
net/http の場合は結局ハンドラを再定義する必要があったのでそこが何とも微妙な感じでした

また interface を元に mock となる構造体を作成するので interface がない場合は今回の方法を使うのは厳しいかなと思います

0 件のコメント:

コメントを投稿