【Go言語】モックを利用したテスト

# はじめに

Goのモックの仕方を学ぶために、以下2つの方法を試していきたいと思います。

* [ライブラリを使わないモック](#ライブラリを使わないモック)
* [gomockを使用したモック](#gomock)

# テスト対象

今回は、以下の**foo.go**の`Fly()`のテストを実装していきたいと思います。

```foo.go
type FourDimensionalPocket interface {
 GetTakecopter() (string, error)
 GetDokodemodoa() (string, error)
}

type Doraemon struct {
}

func (d Doraemon) GetTakecopter() (string, error) {
 return "タケコプター🚁", nil
}

func (d Doraemon) GetDokodemodoa() (string, error) {
 return "どこでもドア🚪", nil
}

func NewDoraemon() Doraemon {
 return Doraemon{}
}

type Nobita struct {
 pocket FourDimensionalPocket
}

type Bird interface {
 Fly() string
}

func NewNobita(pocket FourDimensionalPocket) Bird {
 return Nobita{
  pocket: pocket,
 }
}

func (n Nobita) Fly() string {
 takecopter, err := n.pocket.GetTakecopter()
 if err != nil {
  return "飛べません🐧"
 }
 return fmt.Sprintf("%sを使って飛んでいく!", takecopter)
}
```

クライアント側で以下のように実行できます。

```go
func main() {
 doraemon := NewDoraemon()
 nobita := NewNobita(doraemon)
 result := nobita.fly()
 fmt.Println(result) // タケコプター🚁を使って飛んでいく!
}
```

このFlyをテストするとき、ライブラリを使わず実装すると、以下のようになります。


# ライブラリを使わないモック

```go:foo_test.go
package foo_test

import (
 "testing"

 "foo"

 "github.com/stretchr/testify/assert"
)

type mockDoraemon struct {
 errFunc string
}

func newMockDoraemon(errFunc string) *mockDoraemon {
 return &mockDoraemon{errFunc: errFunc}
}

func (d *mockDoraemon) GetTakecopter() (string, error) {
 if d.errFunc == "GetTakecopter" {
  return "", assert.AnError
 }
 return "タケコプター🚁", nil
}

func (d *mockDoraemon) GetDokodemodoa() (string, error) {
 if d.errFunc == "GetDokodemodoa" {
  return "", assert.AnError
 }
 return "どこでもドア🚪", nil
}

func TestNobita_fly(t *testing.T) {
 tests := []struct {
  name    string
  want    string
  errFunc string
 }{
  {
   name:    "Success Test",
   want:    "タケコプター🚁を使って飛んでいく!",
   errFunc: "",
  },
  {
   name:    "Fail Test",
   want:    "飛べません🐧",
   errFunc: "GetTakecopter",
  },
 }
 for _, tt := range tests {
  t.Run(tt.name, func(t *testing.T) {

   doraemon := newMockDoraemon(tt.errFunc)
   nobita := foo.NewNobita(doraemon)
   if got := nobita.Fly(); got != tt.want {
    t.Errorf("Nobita.Fly() = %v, want %v", got, tt.want)
   }

  })
 }
}
```

ここでは、以下のようにnewMockDoraemonと実体を作成しておりますが、

```go
func newMockDoraemon(errFunc string) *mockDoraemon {
 return &mockDoraemon{errFunc: errFunc}
}
```

実際は以下のように、抽象を渡しているので、わざわざ実体を作る必要はありません。

```go
func NewNobita(pocket FourDimensionalPocket) Bird {
 return Nobita{
  pocket: pocket,
 }
}
```

ゆえに、先ほどのテストは、以下のようにもかけます。

# gomock

https://github.com/uber-go/mock

gomockとはGoogleが開発したモックライブラリ。Googleのリポジトリは、public archiveされており、現在はUberがforkして管理している。

古いリポジトリ : https://github.com/golang/mock

## インストール

```sh
go install go.uber.org/mock/mockgen@latest
```

## バージョン確認

```sh
$ mockgen -version
v0.4.0
```

## モック自動生成

以下のコマンドでモックを自動生成してくれます。

```sh
mockgen -source=***.go
```

先ほどの`foo.go`のモックを作りたい場合は、以下のようにします。

```sh
mockgen -source=foo.go -destination=mock/mock_foo.go -exclude_interfaces=Bird
```

出力先を`mock/mock_foo.go`に指定して、`Bird`のinterfaceはテストで使用しないので、今回は除外しています。

以下は自動生成した`mock_foo.go`です。長いので折りたたみました。

<details><summary> mock/mock_foo.go </summary>

```go:mock/mock_foo.go
// Code generated by MockGen. DO NOT EDIT.
// Source: foo.go
//
// Generated by this command:
//
// mockgen -source=foo.go -destination=mock/mock_foo.go
//

// Package mock_foo is a generated GoMock package.
package mock_foo

import (
 reflect "reflect"

 gomock "go.uber.org/mock/gomock"
)

// MockFourDimensionalPocket is a mock of FourDimensionalPocket interface.
type MockFourDimensionalPocket struct {
 ctrl     *gomock.Controller
 recorder *MockFourDimensionalPocketMockRecorder
}

// MockFourDimensionalPocketMockRecorder is the mock recorder for MockFourDimensionalPocket.
type MockFourDimensionalPocketMockRecorder struct {
 mock *MockFourDimensionalPocket
}

// NewMockFourDimensionalPocket creates a new mock instance.
func NewMockFourDimensionalPocket(ctrl *gomock.Controller) *MockFourDimensionalPocket {
 mock := &MockFourDimensionalPocket{ctrl: ctrl}
 mock.recorder = &MockFourDimensionalPocketMockRecorder{mock}
 return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFourDimensionalPocket) EXPECT() *MockFourDimensionalPocketMockRecorder {
 return m.recorder
}

// GetDokodemodoa mocks base method.
func (m *MockFourDimensionalPocket) GetDokodemodoa() (string, error) {
 m.ctrl.T.Helper()
 ret := m.ctrl.Call(m, "GetDokodemodoa")
 ret0, _ := ret[0].(string)
 ret1, _ := ret[1].(error)
 return ret0, ret1
}

// GetDokodemodoa indicates an expected call of GetDokodemodoa.
func (mr *MockFourDimensionalPocketMockRecorder) GetDokodemodoa() *gomock.Call {
 mr.mock.ctrl.T.Helper()
 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDokodemodoa", reflect.TypeOf((*MockFourDimensionalPocket)(nil).GetDokodemodoa))
}

// GetTakecopter mocks base method.
func (m *MockFourDimensionalPocket) GetTakecopter() (string, error) {
 m.ctrl.T.Helper()
 ret := m.ctrl.Call(m, "GetTakecopter")
 ret0, _ := ret[0].(string)
 ret1, _ := ret[1].(error)
 return ret0, ret1
}

// GetTakecopter indicates an expected call of GetTakecopter.
func (mr *MockFourDimensionalPocketMockRecorder) GetTakecopter() *gomock.Call {
 mr.mock.ctrl.T.Helper()
 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTakecopter", reflect.TypeOf((*MockFourDimensionalPocket)(nil).GetTakecopter))
}

// MockBird is a mock of Bird interface.
type MockBird struct {
 ctrl     *gomock.Controller
 recorder *MockBirdMockRecorder
}

// MockBirdMockRecorder is the mock recorder for MockBird.
type MockBirdMockRecorder struct {
 mock *MockBird
}

// NewMockBird creates a new mock instance.
func NewMockBird(ctrl *gomock.Controller) *MockBird {
 mock := &MockBird{ctrl: ctrl}
 mock.recorder = &MockBirdMockRecorder{mock}
 return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockBird) EXPECT() *MockBirdMockRecorder {
 return m.recorder
}

// Fly mocks base method.
func (m *MockBird) Fly() string {
 m.ctrl.T.Helper()
 ret := m.ctrl.Call(m, "Fly")
 ret0, _ := ret[0].(string)
 return ret0
}

// Fly indicates an expected call of Fly.
func (mr *MockBirdMockRecorder) Fly() *gomock.Call {
 mr.mock.ctrl.T.Helper()
 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fly", reflect.TypeOf((*MockBird)(nil).Fly))
}
```

</details>

テスト自体の実装は以下のようになります。

```go
package foo_test

import (
 "testing"

 "foo"
 mock_foo "foo/mock"
 "github.com/stretchr/testify/assert"
 gomock "go.uber.org/mock/gomock"
)

func TestNobitaFly(t *testing.T) {
 ctrl := gomock.NewController(t)
 defer ctrl.Finish()

 mockPocket := mock_foo.NewMockFourDimensionalPocket(ctrl)

 tests := []struct {
  name        string
  mockReturn  string
  mockError   error
  expectedFly string
 }{
  {
   name:        "正常なケース",
   mockReturn:  "タケコプター🚁",
   mockError:   nil,
   expectedFly: "タケコプター🚁を使って飛んでいく!",
  },
  {
   name:        "エラーケース",
   mockReturn:  "",
   mockError:   assert.AnError,
   expectedFly: "飛べません🐧",
  },
 }

 for _, tt := range tests {
  t.Run(tt.name, func(t *testing.T) {
   mockPocket.EXPECT().GetTakecopter().Return(tt.mockReturn, tt.mockError).Times(1)
   nobita := foo.NewNobita(mockPocket)
   assert.Equal(t, tt.expectedFly, nobita.Fly())
  })
 }
}
```

# gomock v.s. pureなmock

モックしなければならないメソッドが増えると、gomockを使った方が楽そうです。(抽象度が適切かどうかはわかりませんが)
また、自分で実装すると、モックは複雑なので、スタブになることが多いと思います。自分で実装するかgomockを使うかは好みになると思うので、プロジェクトで話し合って決めてください。

# その他のモックライブラリ

ほかにもあるが、moqはスタブを作るだけのライブラリらしい。(参考 :[gomock と比較される moq，こいつはモックを作るライブラリじゃなくてスタブを作るだけの責務放棄したライブラリや！](https://zenn.dev/mpyw/scraps/feffc9b3760000))

* https://github.com/vektra/mockery
* https://github.com/matryer/moq