6
2

More than 3 years have passed since last update.

[golang] mockery/testify mockを作るのに困ったとき

Last updated at Posted at 2020-03-09

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 さんがもっと具体的なコードを書いてくださってますので、こういう記事に興味があるかたはそちらも併せてどうぞ!

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2