mockery : https://github.com/vektra/mockery
testify: https://github.com/stretchr/testify
上記のモック作成ツールを使ったテストを書いているとき、mockを定義しようとして「困ったな」と思う事が時々あります。そんなときに使えそうな方法について書いてみました。
それぞれの解答に対して評価(〇、△、X)をつけていますが、あくまでこれは主観的なものです。異見があるかたはコメントください。
問題1(副作用)
さて、golangでこんなモジュールがあったとします。
type Sample struct {
}
func (s *Sample) Hoge(in interface{}, out interface{}) error {
// do something
}
in に入力を受け取り、outに出力内容を書きこんで返してくるメソッドです。
たとえばinを
json.Marshal
して作成したbodyを使ってhttpリクエストし、戻ってきたレスポンスをjson.Unmarshal
でoutに書き込んで返してくるイメージ。
さて、これをモックするとします。
mockSample := new(mocks.Sample)
in := &Input{something}
mockSample.On("Hoge", in, mock.Anything).Return(nil)
ここで困ってしまいます。Hogeはoutの中身を書き換えて返す関数ですが、これをどうモックで表現したらいいんでしょう?
解答1(X)
mockSample := new(mocks.Sample)
in := &Input{something}
mockSample.On("Hoge", in, mock.Anything).Return(nil)
何もしない。もちろんテストがそもそも通過しません。
解答2(〇)
mockSample.On("Hoge", in, out).Return(nil).Once().Run(func(args mocks.Argument) {
o := args[2].(*Output)
*o = Output{something}
})
こうすることでこのmockは引数に値を出力することができそうです。このRun()に渡している関数はRunFnと定義されているようですね。
何をしているかといいますと、Runの中ではmockがリクエストを受けたとき行う処理を書くことができます。そこで、引数として受け取ったoutのアドレスの参照先に戻したい値の「実体」をコピーしています。
戻り値で戻すならReturnに書くだけなので簡単ですが、引数による返却は少し難しいです・・・ですがargumentsを引数に受け取って何かする関数であるRunFnは、基本的にこのような事を目的とするもののように思われます。
解答3(X)
mockSample.On("Hoge", in, mock.MatchedBy(func(out interface{}) bool) {
o := out.(*Output)
*o = Output{something}
return true
})).Return(nil)
MatchedByを使ってこんなこともできそうではあるものの、MatchedByはあくまでmatcherなので、matcher以外の用途(副作用)に使うべきではないかと思います。
問題2(関数引数)
package sample
type Sample interface {
Hoge(func() string) string
Fuga() string
}
type sample struct{}
func (s *sample) Hoge(f func() string) string {
return f()
}
func (s *sample) Fuga() string {
return "Fuga"
}
type Sample2 struct {
s Sample
}
func (s *Sample2) Piyo() string {
return s.s.Hoge(func() string {
return s.s.Fuga()
})
}
ちょっと長いのですが、Hoge(func() string) string
こんなものをモックする方法についていろいろ試してみました。
解答1(X)
m := new(mocks.Sample)
m.On("Fuga").Return("Mock2")
m.On("Hoge", mock.Anything).Return("Mock")
もちろんこれだけだとHogeの引数である関数をテストしていませんのでダメです。
解答2(△)
m := new(mocks.Sample)
m.On("Fuga").Return("Mock2")
m.On("Hoge", mock.Anything).Return("Mock").Run(func(args mock.Arguments){
f := args[0].(func() string)
assert.Equal(t, "Mock2", f())
})
モック定義の中でアサートしている事が違和感があります。
解答2(△)
m := new(mocks.Sample)
m.On("Fuga").Return("Mock2")
m.On("Hoge", mock.Anything).Return(func(f func() string) string {
return f()
})
ReturnValueProviderFunctionです。
シンプルではあるものの、この関数、Hogeそのものなんです。
「単体テスト」的に、テストの中でmockの実装に依存するのは抵抗があります。ただまあ、限りなく〇に近い△です。
解答3(X)
m := new(mocks.Sample)
m.On("Fuga").Return("Mock2")
m.On("Hoge", mock.MatchedBy(func(f func() string)bool{
fmt.Println(f())
return true
})).Return("Mock")
これはdeadlockになります。というのは、Matcherのなかでmock自身が呼ばれるとダメのようです。
RunやReturnValueProviderFunctionだとdeadlockにならないんですが・・・
MatchedByは関数引数には無力です。
解答4(〇)
m := new(mocks.Sample)
m.On("Fuga").Return("Mock2")
m.On("Hoge", mock.Anything).Return("Mock")
// test
sut := &Sample2{m}
assert.Equal(t, "Mock", sut.Piyo())
f := m.Calls[0].Arguments[0].(func()string)
assert.Equal(t, "Mock", f())
- Anythingで荒くassertする
- そのあと、改めてargsのチェックをする
結論
いろいろ方法はあるかと思いますが、困ったときは「単体テストとは何か」に立ち返ってみるのが良いと思います。
そのうえで、RunやMatchedByやReturnValueProviderFunction、そしてテストそのものの分離を検討してみてください。
余
m.AssertExpectations()
こんな記事を読むような方には言わずもがなと思いますが、コール数のチェックをお忘れなく!
参照
https://qiita.com/tomtwinkle/items/55f79c969d48206c9945
上記で @tomtwinkle さんがもっと具体的なコードを書いてくださってますので、こういう記事に興味があるかたはそちらも併せてどうぞ!