moqでinterfaceのMockを作る
この記事は Go3 Advent Calendar 2017の18日目の記事です。
moqでMockを作る
Goでアプリケーションを書いているとinterfaceにMockを投げ込みたくなる瞬間があると思います。
たとえば、以下のようなコードのテストを書きたい場合ですね。
User
を取りに行く処理が外部に依存しているためにClient
interfaceを切っていて、それをApplication
構造体が持っています。
type User struct { ID int FirstName string LastName string } type Client interface { GetAllUser() ([]*User, error) } type Application struct { Client Client } func (a *Application) FilterLastName(name string) ([]*User, error) { users, _ := a.Client.GetAllUser() var result []*User for _, u := range users { if u.LastName == name { result = append(result, u) } } return result, nil }
Goはダックタイピングなので、以下のようにClient
interfaceを満たす構造体を自分で定義するのもいいですが、
interfaceが持つメソッドが多いと自分で定義するのも面倒です。
type Mock struct { FilterLastNameFunc func(string) ([]*User, error) } func (m *Mock) FilterLastName(name string) ([]*User, error) { return m.FilterLastNameFunc(name) }
となれば「楽をしよう!」ということで、この記事ではMockを自動生成してくれる moqを紹介します。
他にも幾つかMockを自動生成してくれるツールはありますが、他のツールに比べてmoqが優れているところは、
自動生成されたMockが余計なパッケージをimportしていない
という点にあります。
とりあえず以下のコマンドでインストールします。
$ go get github.com/matryer/moq
インストールできたらMockを生成してみます。
$ moq -out client_mock.go . Client
以下が生成したclient_mock.goの内容です。
// Code generated by moq; DO NOT EDIT // github.com/matryer/moq package main import ( "sync" ) var ( lockClientMockGetAllUser sync.RWMutex ) // ClientMock is a mock implementation of Client. // // func TestSomethingThatUsesClient(t *testing.T) { // // // make and configure a mocked Client // mockedClient := &ClientMock{ // GetAllUserFunc: func() ([]*User, error) { // panic("TODO: mock out the GetAllUser method") // }, // } // // // TODO: use mockedClient in code that requires Client // // and then make assertions. // // } type ClientMock struct { // GetAllUserFunc mocks the GetAllUser method. GetAllUserFunc func() ([]*User, error) // calls tracks calls to the methods. calls struct { // GetAllUser holds details about calls to the GetAllUser method. GetAllUser []struct { } } } // GetAllUser calls GetAllUserFunc. func (mock *ClientMock) GetAllUser() ([]*User, error) { if mock.GetAllUserFunc == nil { panic("moq: ClientMock.GetAllUserFunc is nil but Client.GetAllUser was just called") } callInfo := struct { }{} lockClientMockGetAllUser.Lock() mock.calls.GetAllUser = append(mock.calls.GetAllUser, callInfo) lockClientMockGetAllUser.Unlock() return mock.GetAllUserFunc() } // GetAllUserCalls gets all the calls that were made to GetAllUser. // Check the length with: // len(mockedClient.GetAllUserCalls()) func (mock *ClientMock) GetAllUserCalls() []struct { } { var calls []struct { } lockClientMockGetAllUser.RLock() calls = mock.calls.GetAllUser lockClientMockGetAllUser.RUnlock() return calls }
Lockの処理があるため、syncパッケージをimportしていますが、それ以外にimportをしているものはありません。
基本的にやっていることはMock
構造体を定義していたときと同じです。
以下のように使うことができます。
mock := &ClientMock{ GetAllUserFunc: func() ([]*User, error) { users := []*User{ &User{ID: 1, FirstName: "Taro", LastName: "Suzuki"}, &User{ID: 2, FirstName: "Taro", LastName: "Tanaka"}, &User{ID: 3, FirstName: "Jiro", LastName: "Sato"}, &User{ID: 4, FirstName: "Saburo", LastName: "Tanaka"}, } return users, nil }, } app := &Application{ Client: mock, } users, err := app.FilterLastName("Tanaka") if err != nil { panic(err) } for _, u := range users { fmt.Printf("%+v\n", u) }
また、~~Calls
というメソッドで、当該のメソッドが何回呼ばれたかもチェックすることができます。
fmt.Printf("call count: %d\n", len(mock.GetAllUserCalls()))
~~Func
変数に値が入っていないときには、そのことを示した上でpanicを発生させてくれるので、
メソッドの数が多いinterfaceのMockを生成したときにも必要な部分だけを実装させるのもそこまで難しくありません。
moqに合わないユースケース
上記例のようなClient
interfaceとそれを持つ構造体が同じパッケージ内部に存在する場合にはとても重宝しますが、
外部のパッケージのMockを生成しようとするとちょっと使いづらいです。
たとえば以下のようにfooパッケージとbarパッケージでそれぞれ提供されている
Client
interfaceを作りたい場合はつらみが出てきます。
type Application struct { FooClient foo.Client BarClient bar.Client }
moqで指定するinterfaceのパスは、Goのimportパスではなく、普通のディレクトリパスのため、 生成されたファイルのimportに相対パスが入り込んでしまいます。
import ( "./vendor/github.com/nametake/foo" "sync" )
また、生成されたMockの構造体は、{interface名}Mock
という名前になるので、上記のように別のパッケージで
同じ名前のinterfaceを利用している場合は、どちらもClientMock
という名前になるので、ビルドでコケてしまいます。
まとめ
ユースケースに合わない部分もありますが、Mockの生成のようなやり方が定形になっている部分は どんどん自動化をして楽をしていきましょう!