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

mockery : https://github.com/vektra/mockery
testify: https://github.com/stretchr/testify
上記のモック作成ツールを使ったテストを書いているとき、mockを定義しようとして「困ったな」と思う事が時々あります。そんなときに使えそうな方法について書いてみました。

> それぞれの解答に対して評価（〇、△、X）をつけていますが、あくまでこれは主観的なものです。異見があるかたはコメントください。

# 問題1（副作用）

さて、golangでこんなモジュールがあったとします。

```go
type Sample struct {
}
func (s *Sample) Hoge(in interface{}, out interface{}) error {
   // do something
}
```

in に入力を受け取り、outに出力内容を書きこんで返してくるメソッドです。
> たとえばinを`json.Marshal`して作成したbodyを使ってhttpリクエストし、戻ってきたレスポンスを`json.Unmarshal`でoutに書き込んで返してくるイメージ。


さて、これをモックするとします。

```go
mockSample := new(mocks.Sample)
in := &Input{something}
mockSample.On("Hoge", in, mock.Anything).Return(nil)
```

ここで困ってしまいます。Hogeはoutの中身を書き換えて返す関数ですが、これをどうモックで表現したらいいんでしょう？

## 解答1（X）

```go
mockSample := new(mocks.Sample)
in := &Input{something}
mockSample.On("Hoge", in, mock.Anything).Return(nil)
```

何もしない。もちろんテストがそもそも通過しません。

## 解答2（〇）

```go
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）

```go
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（関数引数）

```go
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)

```go
	m := new(mocks.Sample)
	m.On("Fuga").Return("Mock2")
	m.On("Hoge", mock.Anything).Return("Mock")
```

もちろんこれだけだとHogeの引数である関数をテストしていませんのでダメです。

## 解答2（△）

```go
	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（△）

```go
	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）

```go
	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（〇）

```go
	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、そしてテストそのものの分離を検討してみてください。

# 余

```go
	m.AssertExpectations()
```

こんな記事を読むような方には言わずもがなと思いますが、コール数のチェックをお忘れなく！

# 参照

https://qiita.com/tomtwinkle/items/55f79c969d48206c9945
上記で @tomtwinkle さんがもっと具体的なコードを書いてくださってますので、こういう記事に興味があるかたはそちらも併せてどうぞ！