これまで実務でちょいちょい使ってたけど、しっかり調べたことなかったので調べてみた。
概要
go generate
向けのインターフェースのモックを生成するツール。
任意のインターフェースから構造体を生成し、インターフェースのモックとしてテストコードで使用できる。
インストール
go install github.com/matryer/moq@latest
モックの生成
以下のコマンドを実行してモックを生成する。
moq [flags] source-dir interface [interface2 [interface3 [...]]]
source-dir
インターフェース定義を記述した Go ファイルが存在するディレクトリへのパス。
interface
インターフェース名。
flags
-fmt string
Go の pretty-printer を指定。
-
gofmt
: (デフォルト) -
goimports
: -
noop
: 何もしない
pretty-printer = 整形表示機能
-out string
モックの出力先ファイルを指定。デフォルトは標準出力。
-pkg string
モックのパッケージ名を指定。
デフォルトは推測による決定。
-rm
出力先にファイルが存在するならば削除してから出力。
-skip-ensure
モックの実装チェックを抑制し、モックがテストされるパッケージの外側で生成される場合、循環インポートを回避する。
-stub
モックの実装が提供されていない場合、panic にならず 0 を返す。
-version
moq コマンドのバージョンを表示。
-with-resets
モックに対して行われた呼び出しのリセットを容易にする関数を生成する。
moq コマンドの仕様
公式のオプションの説明だけだと、仕様がよくわからなかったため、ソースコードを読んだり実験して確認してみた。
モック構造体の名前
インターフェース名をコマンド上で XxxXxx
と指定するならば、モック構造体名は XxxXxxMock
に。
:
を挟んで指定する場合、例えばXxxXxx:YyyYyy
と指定するならば、モック構造体名は YyyYyy
に。
つまり、明示的に指定できる。
モック構造体のパッケージ名
-pkg
オプションで指定しているならばその値、なければインターフェースのパッケージ名と同名
-stub
の効果
フィールド値を置き換えていないモック構造体のメソッドが呼ばれた場合**
-
-stub
オプションを指定している場合: そのメソッドの返り値としてゼロ値を返す -
-stub
オプションを指定していない場合: panic になる
-stub
なし
func (mock *SampleMock) Now() time.Time {
if mock.NowFunc == nil {
panic("SampleMock.NowFunc: method is nil but Sample.Now was just called")
}
callInfo := struct {
}{}
mock.lockNow.Lock()
mock.calls.Now = append(mock.calls.Now, callInfo)
mock.lockNow.Unlock()
return mock.NowFunc()
}
-stub
あり
// Now calls NowFunc.
func (mock *SampleMock) Now() time.Time {
callInfo := struct {
}{}
mock.lockNow.Lock()
mock.calls.Now = append(mock.calls.Now, callInfo)
mock.lockNow.Unlock()
if mock.NowFunc == nil {
var (
timeOut time.Time
)
return timeOut
}
return mock.NowFunc()
}
-skip-ensure
の効果
その構造体が特定のインターフェースを実装しているか?を確認するためによく記述する
var _ <インターフェース名> = &<対象の構造体名> {}
を出力しないようにする。
(これを出力すると、インターフェースが定義されているファイルの import が必要になり、結果として望まない循環参照が発生しやすくなる)
-skip-ensure
あり:
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package mock
import (
"sync"
"time"
)
// SampleMock is a mock implementation of sample.Sample.
//
// func TestSomethingThatUsesSample(t *testing.T) {
//
// // make and configure a mocked sample.Sample
// mockedSample := &SampleMock{
// NowFunc: func() time.Time {
// panic("mock out the Now method")
// },
// }
//
// // use mockedSample in code that requires sample.Sample
// // and then make assertions.
//
// }
type SampleMock struct {
// NowFunc mocks the Now method.
NowFunc func() time.Time
// calls tracks calls to the methods.
calls struct {
// Now holds details about calls to the Now method.
Now []struct {
}
}
lockNow sync.RWMutex
}
-skip-ensure
なし
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package mock
import (
"github.com/fnami0316/moq_sample/sample"
"sync"
"time"
)
// Ensure, that SampleMock does implement sample.Sample.
// If this is not the case, regenerate this file with moq.
var _ sample.Sample = &SampleMock{}
// SampleMock is a mock implementation of sample.Sample.
//
// func TestSomethingThatUsesSample(t *testing.T) {
//
// // make and configure a mocked sample.Sample
...
-with-resets
モック構造体は、各メソッドごとにそのメソッドを呼び出したときに、空の構造体がそのメソッド用のスライスに追加されていく挙動になっていて、XxxCalls というメソッドでそのスライスを取得できる。
このオプションを指定することで、各メソッド用のスライスを空にするためのメソッド ResetXxxCalls
や、すべてのスライスを空にするためのメソッド ResetCalls
を追加で出力してくれる。
-with-resets
あり
// ResetNowCalls reset all the calls that were made to Now.
func (mock *SampleMock) ResetNowCalls() {
mock.lockNow.Lock()
mock.calls.Now = nil
mock.lockNow.Unlock()
}
// ResetCalls reset all the calls that were made to all mocked methods.
func (mock *SampleMock) ResetCalls() {
mock.lockNow.Lock()
mock.calls.Now = nil
mock.lockNow.Unlock()
}
-rm
の効果
公式の説明どおり、モックのファイルを出力する前にファイルを削除するだけ。
別にこのオプションを指定しなくても上書きされる(適当に生成コードにコメント書いたりしてみたが)ため、存在意義がよくわからなかった。
メソッド挙動の指定
各インターフェースのメソッド Xxx
の挙動を指定する場合、 モック構造体の XxxFunc
というフィールドを差し替える。
差し替えなかった場合に、モックを通じてそのメソッドが呼び出されると panic になる(※)。
※先述の通り、-stub
を指定していれば、そのメソッドのゼロ値の返り値が返ってくるだけ。
実例
sample/sample.go
の Sample インターフェースのモック構造体の定義を mock/sample.go
に出力させるように、sample/mock/gen.go
に、go:generate
ディレクティブを記述してある(オプション全盛りにしてある)
また、生成したモックを使ってテストコードも記述している。
プロジェクトルートで go generate ./...
を実行するとモックが出力されるので、
go:generate
ディレクティブ内の moq のオプションを変えたりして実験すると理解が深まるかと思う。
参考