Golang mockery/testifyで高階関数をmockする

golangでテストを書く際、[vektra/mockery](https://github.com/vektra/mockery)による[stretchr/testify](https://github.com/stretchr/testify)で使用できるmockの自動生成が非常に便利なのですが、双方クイックリファレンス以外の細かいドキュメントがあまり存在せず特殊な関数をmockしようとする結構悩みます。

最近調べまくったもので**「無名関数で書かれた高階関数のmock」**はズバリの解決方法が検索してもtestifyのissueくらいしか出てこなかったので同じく悩める人のためにここに残しておきます。

# ポイント かつ 結論
* 引数を元にmock内の処理を独自に書きたいなら[Call.Run] (https://godoc.org/github.com/stretchr/testify/mock#Call.Run)を活用する
* mockの戻り値を動的に変更したいなら[Call.Return](https://godoc.org/github.com/stretchr/testify/mock#Call.Return)を活用する

# 高階関数のtestify mockサンプル

シンプルに書こうと思いまいしたがテスト容易にするDIのためのinterface定義を必ず書く都合上結構記述量が多くなりました。
[githubにコード公開](https://github.com/tomtwinkle/go-mockery-mock-sample/tree/master/anonymous_func)しておいたのでそっち見たほうが理解が早いかもしれません。

## プロダクトコード

まずはテストしたいfuncである `ExampleAnonymousFunc()`は以下のように高階関数である`WithFunc()`の中に実際のロジックが記述されています。

```go:funcs/example.go
package funcs

import (
	"fmt"
	"github.com/pkg/errors"
	"log"
)

type Example interface {
	ExampleAnonymousFunc() (string, error)
}

type example struct {
	called ExampleCalled
}

func NewExample(called ExampleCalled) Example {
	return &example{called: called}
}

func (e *example) ExampleAnonymousFunc() (string, error) {
	log.Print("start ExampleAnonymousFunc()")
	v, err := e.called.WithFunc(func() (i interface{}, err error) {
		// ここにロジックが色々書いてある
		v, err := e.called.Example()
		log.Print(fmt.Sprintf("called anonymous func! result: %s", v))
		// ここにロジックが色々書いてある
		return v, errors.WithStack(err)
	})
	if err != nil {
		return "", errors.WithStack(err)
	}
	log.Print(fmt.Sprintf("end ExampleAnonymousFunc() result: %s", v))
	return v.(string), nil
}
```

`ExampleAnonymousFunc()`内で使用している`WithFunc()`のmockが今回の悩みどころ。
普通に`WithFunc()`自体をmockしてしまうと色々書いてあるロジック自体がテストされません。

次にmockされる側の関数`WithFunc()`と`Example()`は以下の通り。

```go:funcs/example_called.go
package funcs

import (
	"github.com/pkg/errors"
	"log"
)

type ExampleCalled interface {
	WithFunc(func() (interface{}, error)) (interface{}, error)
	Example() (string, error)
}
type exampleCalled struct{}

func NewExampleCalled() ExampleCalled {
	return &exampleCalled{}
}

func (e *exampleCalled) WithFunc(
	fn func() (interface{}, error),
) (interface{}, error) {
	log.Print("called with func!")
	v, err := fn()
	if err != nil {
		return nil, errors.WithStack(err)
	}
	return v, nil
}

func (e *exampleCalled) Example() (string, error) {
	log.Print("called example!")
	return "example!!!", nil
}
```

こっちのテストはtestifyのクイックリファレンスの範疇内で簡単に出来るので今回は触れません。

main関数を書いて実行してみます。

```go:main.go
package main

import (
	"fmt"
	"go-mockery-mock-sample/anonymous_func/funcs"
	"log"
)

func main() {
	log.Print("start")
	ex := funcs.NewExample(funcs.NewExampleCalled())

	if result, err := ex.ExampleAnonymousFunc(); err != nil {
		log.Print("error!")
	} else {
		log.Print(fmt.Sprintf("result: %s", result))
	}
	log.Print("end")
}
```

```
2020/03/07 21:36:01 start
2020/03/07 21:36:01 start ExampleAnonymousFunc()
2020/03/07 21:36:01 called with func!
2020/03/07 21:36:01 called example!
2020/03/07 21:36:01 called anonymous func! result: example!!!
2020/03/07 21:36:01 end ExampleAnonymousFunc() result: example!!!
2020/03/07 21:36:01 result: example!!!
2020/03/07 21:36:01 end
```

`WithFunc()`と`Example()`双方がmockされ、無名関数内のコードがテストされれば
`called with func!`と`called example!`の部分が出力されなくなるはずですが、さて。

## テストコード(NGパターン)

まずは駄目な例から

```go:funcs/example_test.go
package funcs

import (
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"go-mockery-mock-sample/anonymous_func/mocks"
	"testing"
)

func TestExample_ExampleAnonymousFunc(t *testing.T) {
	t.Run("example NG", func(t *testing.T) {
		mockExampleCalled := new(mocks.ExampleCalled)

		mockExampleCalled.On("WithFunc", mock.Anything).Return("WithFunc mock!", nil)
		mockExampleCalled.On("Example").Return("Example mock!", nil)

		e := NewExample(mockExampleCalled)
		actual, err := e.ExampleAnonymousFunc()
		assert.NoError(t, err)
		assert.Equal(t, "WithFunc mock!", actual)
		mockExampleCalled.AssertExpectations(t)
	})
}
```

```
=== RUN   TestExample_ExampleAnonymousFunc
=== RUN   TestExample_ExampleAnonymousFunc/example_NG
2020/03/07 21:44:36 start ExampleAnonymousFunc()
2020/03/07 21:44:36 end ExampleAnonymousFunc() result: WithFunc mock!
    TestExample_ExampleAnonymousFunc/example_NG: example_test.go:21: PASS:	WithFunc(string)
    TestExample_ExampleAnonymousFunc/example_NG: example_test.go:21: FAIL:	Example()
        		at: [example_test.go:15]
    TestExample_ExampleAnonymousFunc/example_NG: example_test.go:21: FAIL: 1 out of 2 expectation(s) were met.
        	The code you are testing needs to make 1 more call(s).
        	at: [example_test.go:21]
--- FAIL: TestExample_ExampleAnonymousFunc (0.02s)
    --- FAIL: TestExample_ExampleAnonymousFunc/example_NG (0.02s)
FAIL
```

`end ExampleAnonymousFunc() result: WithFunc mock!` が出ているので`WithFunc()`は無事mockされました。
しかし、`Example()`が呼び出されずにFailしました。

先程も書きましたが、`ExampleAnonymousFunc()`内で使用している`WithFunc()`のmockを普通に行ってしまうと、無名関数内に記述された色々な処理がmockされてしまいます。`Example()`も呼ばれません。

## テストコード(OKパターン)

前置きが長くなりましたがここが本題です。
`WithFunc()`をmockしつつ引数で渡した無名関数は実行させ、その戻り値を`WithFunc()`の戻り値として使用するためにはtestifyの`RunFn`や`Run`を使用します。

```go:funcs/example_test.go
package funcs

import (
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"go-mockery-mock-sample/anonymous_func/mocks"
	"testing"
)

func TestExample_ExampleAnonymousFunc(t *testing.T) {
	t.Run("example OK", func(t *testing.T) {
		mockExampleCalled := new(mocks.ExampleCalled)

		mockWithFunc := mockExampleCalled.On("WithFunc", mock.Anything)
		mockWithFunc.Run(func(args mock.Arguments) {
			// WithFuncの処理をmockしつつ、高階関数の引数の関数実行は行う
			fn := args[0].(func() (interface{}, error))
			v, err := fn()
			// 高階関数の引数の関数の戻り値を返す
			mockWithFunc.ReturnArguments = mock.Arguments{v, err}
		})
		mockExampleCalled.On("Example").Return("Example mock!", nil)

		e := NewExample(mockExampleCalled)
		actual, err := e.ExampleAnonymousFunc()
		assert.NoError(t, err)
		assert.Equal(t, "Example mock!", actual)
		mockExampleCalled.AssertExpectations(t)
	})
}
```

```
=== RUN   TestExample_ExampleAnonymousFunc
=== RUN   TestExample_ExampleAnonymousFunc/example_OK
2020/03/07 21:52:48 start ExampleAnonymousFunc()
2020/03/07 21:52:48 called anonymous func! result: Example mock!
2020/03/07 21:52:48 end ExampleAnonymousFunc() result: Example mock!
    TestExample_ExampleAnonymousFunc/example_OK: example_test.go:41: PASS:	WithFunc(string)
    TestExample_ExampleAnonymousFunc/example_OK: example_test.go:41: PASS:	Example()
--- PASS: TestExample_ExampleAnonymousFunc (0.02s)
    --- PASS: TestExample_ExampleAnonymousFunc/example_OK (0.02s)
PASS
```

`WithFunc()`をmockしつつちゃんと`Example()`も実行されました。

めでたしめでたし

# サンプルコード
今回使用したサンプルコード
[tomtwinkle/go-mockery-mock-sample](https://github.com/tomtwinkle/go-mockery-mock-sample/tree/master/anonymous_func)

# 参考サイト
[Allow dynamic returns based on arguments | testify/issues/350](https://github.com/stretchr/testify/issues/350#issuecomment-570478958)

@kishibashi3 さんがもう少し汎用的に用例を書いてくれました。
[[golang] mockery/testify mockを作るのに困ったとき](https://qiita.com/kishibashi3/items/64e1c1a8af639a95ae3f)