gomockでmockを作ってみる[Go]

# gomockとは?
gomockとは、インターフェース定義からモックの生成を行うことができるgoのライブラリで、go公式から出されています。

https://github.com/golang/mock

しかし、READMEには

>Update, June 2023: This repo and tool are no longer maintained. Please see go.uber.org/mock for a maintained fork instead.

と書かれており2023でメンテナンスが終了しているようでした。

READMEにも書かれているように`go.uber.org/mock`がその代わりとして使用されています。

`go.uber.org/mock`は、かの有名なUberEatsを運営しているUberが`gomock`のメンテナンスを引き継いだライブラリです。

今回は、`go.uber.org/mock`でモックを作成してみようと思います。

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


# そもそもモックって?

個人開発のみをしている学生やプログラミングやりたての人からすると、あまり聞き慣れた単語ではないと思います。

モックは、その名の通り模型のことで、アプリケーションのテストをするために必要な部品を作成することを言います。

使用ケースとしては、データベースなどの外部に影響を受ける部分をテストする場合に、テストしたい部分がデータベースからデータを取り出すことでないことがあります。その場合は、取り出せたと仮定して動いてくれた方がテストで調べたい内容を正確に調べることができるため使用されます。

こう説明してみても、抽象的でわかりずらいと思うので、実際に実装してみようと思います。

# 実装してみる

userの登録機能を例に、ユースケース層でのテストを実装してみようと思います。

例えば、下のような感じの構造で実装したとします。

```
.
├── domain
│   ├── entity
│   ├── mockreposiotry
│   ├── mockrepository
│   └── repository
├── infrastructure
├── main.go
├── repository
└── service(ユースケース)

```

ただ、今回のようにユースケースでテストを行うだけなら下のような感じでも動きます。
```
.
├── domain
│   ├── entity
│   ├── mockreposiotry
│   ├── mockrepository
│   └── repository
├── main.go
├── repository
└── service(ユースケース)
```


## 下準備

今回は、ユースケースでのテスト実装に焦点を当てるので、最小限の実装コードのみを載せます。

- Userモデル定義
dbから取り出して扱うGoの構造体を定義します。
```domain/enitity/user.go
package entity

type User struct {
	ID   int
	Name string
}
```

- repositoryのインターフェース
dbから取り出す操作をインターフェースとして抽象定義しています。
```domain/repository/user.go
package repository

import "example.com/user/domain/entity"

type IUserRepository interface {
	CreateUser(name string) (*entity.User, error)
}

```

- repositoryの実装
先ほど定義したインターフェースの中身を実装しています。
```repository/user.go
package repository

import (
	"example.com/user/domain/entity"
	"gorm.io/gorm"
)

type UserRepository struct {
	db *gorm.DB
}

func NewUserRepository(db *gorm.DB) *UserRepository {
	return &UserRepository{db: db}
}

func (r *UserRepository) CreateUser(name string) (*entity.User, error) {

	user := &entity.User{Name: name}

	r.db.Create(user)

	return user, nil
}
```

:::note
Goのインターフェース継承は、`ダックタイピング`で行われます。
`ダックタイピング`は、インターフェースに定義したメソッドを実装した構造体があれば、自動的にインターフェースが継承されたとみなされます。
:::

- serviceの実装
ユースケースを実装しています。
ここでは、repositoryインターフェースからデータを取り出すことを行っています。
```service/user.go
package service

import (
	"example.com/user/domain/entity"
	"example.com/user/domain/repository"
)

type UserService struct {
	repo repository.IUserRepository
}

func NewUserService(repo repository.IUserRepository) *UserService {
	return &UserService{repo: repo}
}

func (s *UserService) CreateUser(name string) (*entity.User, error) {
	user, err := s.repo.CreateUser(name)
	if err != nil {
		return nil, err
	}
	return user, nil
}

```

## モックの作成
ここから実際にモックを作成してみます。

### インストール

`go.uber.org/mock`のインストールは下のようにします。
```
go get go.uber.org/mock/mockgen
go install go.uber.org/mock/mockgen@latest
```
>`go.uber.org/mock`の[GithubリポジトリのREADME](https://github.com/uber-go/mock?tab=readme-ov-file)に`go get`の文面見つからないかもしれませんがつけてください。

:::note
`go install`だけだと依存関係を管理する`go mod`には追加されません。
go getも行ってください
:::

インストールできたら下のコマンドで入っているかを確認します。
```
mockgen -version
```

:::note
もし入っていなかった場合は、GOPATHを通してください
```
export PATH=$PATH:$(go env GOPATH)/bin
```
:::

### モックを生成する

下のコマンドでモックを作成します。

```
mockgen -source=domain/repository/user.go -destination=domain/mockreposiotry/user.go -package mockrepository
```
`-source`でモックにしたいインターフェースが定義されているファイルを指定します。
`-destination`でモックを生成する場所を指定します。
`package`でモックのパッケージ名を指定します。

生成されたコードは下のようになります。

```go
// Code generated by MockGen. DO NOT EDIT.
// Source: domain/repository/user.go
//
// Generated by this command:
//
//	mockgen -source=domain/repository/user.go -destination=domain/mockreposiotry/user.go -package mockrepository
//

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

import (
	reflect "reflect"

	entity "example.com/user/domain/entity"
	gomock "go.uber.org/mock/gomock"
)

// MockIUserRepository is a mock of IUserRepository interface.
type MockIUserRepository struct {
	ctrl     *gomock.Controller
	recorder *MockIUserRepositoryMockRecorder
}

// MockIUserRepositoryMockRecorder is the mock recorder for MockIUserRepository.
type MockIUserRepositoryMockRecorder struct {
	mock *MockIUserRepository
}

// NewMockIUserRepository creates a new mock instance.
func NewMockIUserRepository(ctrl *gomock.Controller) *MockIUserRepository {
	mock := &MockIUserRepository{ctrl: ctrl}
	mock.recorder = &MockIUserRepositoryMockRecorder{mock}
	return mock
}

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

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

// CreateUser indicates an expected call of CreateUser.
func (mr *MockIUserRepositoryMockRecorder) CreateUser(name any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockIUserRepository)(nil).CreateUser), name)
}

```

:::note
コメントアウトにも書かれていますが、このファイルは編集するのはダメです。
:::

### モックを使ってみる

実際に生成したモックでテストを書いてみます。
```go
package service

import (
	"testing"

	"example.com/user/domain/entity"
	mockrepository "example.com/user/domain/mockreposiotry"
	gomock "go.uber.org/mock/gomock"
)

func TestCreateUser(t *testing.T) {
	ctrl := gomock.NewController(t)

	mr := mockrepository.NewMockIUserRepository(ctrl)

	svc := NewUserService(mr)

	mr.EXPECT().CreateUser("John").Return(&entity.User{ID: 1, Name: "John"}, nil).Times(1)

	user, err := svc.CreateUser("John")
	if err != nil {
		t.Errorf("Error was not expected, got: %v", err)
	}

	if user.Name != "John" {
		t.Errorf("User name is not correct, got: %s", user.Name)
	}
	if user.ID != 1 {
		t.Errorf("User ID is not correct, got: %v", user.ID)
	}

}
```

実行してみると下のようになります。
```
go test -v
=== RUN   TestCreateUser
--- PASS: TestCreateUser (0.00s)
PASS
ok  	example.com/user/service	0.411s
```

#### テストコードの説明
まず、モックの呼び出しに必要なコントローラーを生成します。
```go
ctrl := gomock.NewController(t)
```

生成したコントローラーを使ってモックリポジトリを生成させます。
```go
mr := mockrepository.NewMockIUserRepository(ctrl)
```

作成したモックを依存関係として、`NewUserService()`にDIすることで、serviceオブジェクトを生成します。
```go
svc := NewUserService(mr)
```

:::note
#### DI(Dependacy Injection)とは
`依存性注入`と呼ばれる方法で、インターフェースを依存関係として入れることで、複雑なオブジェクトを依存させない方法です。

オブジェクト指向プログラミングでは、コンストラクタにインスタンスの生成に必要な依存関係を記述します。
Goではコンストラクタにインターフェースが依存関係にあることを明記することで、実際にインスタンスを生成するときにインターフェースを継承したものであればどんなものでもインスタンスメソッドの引数に入れることができます。

今回でいうと`NewUserService()`の引数は、インターフェースを継承した`MockIUserRepository`も`NewUserRepository`も入れてインスタンスを生成することが可能です。
:::

モックリポジトリにデータを入れます。
```go
mr.EXPECT().CreateUser("John").Return(&entity.User{ID: 1, Name: "John"}, nil).Times(1)
```

`EXPECT()`は、mockgenで生成させた`mockrepository/user.go`の中で下のように定義されています。
ここでは、recorder(今回だと`MockIUserRepositoryMockRecorder`)を返しています
```go
func (m *MockIUserRepository) EXPECT() *MockIUserRepositoryMockRecorder {
	return m.recorder
}
```

`CreateUser()`は、実は生成されたモックに2つ定義されています。
```go
func (m *MockIUserRepository) CreateUser(name string) (*entity.User, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "CreateUser", name)
	ret0, _ := ret[0].(*entity.User)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

func (mr *MockIUserRepositoryMockRecorder) CreateUser(name any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockIUserRepository)(nil).CreateUser), name)
}
```

直前の`EXPECT()`でrecorderを返しているので、今回は下の`CreateUser()`が使用されています。


`Return()`では、モックが結果的にどのようなデータを返すかを定義しています。
今回は、モックリポジトリがdbにIDが1の名前が"John"という人のデータを入れたという仮の結果を想定しています。

`Times(1)`では、使用するサービスのメソッド内で何回`CreateUser()`が呼び出されるかを制御しています。

再掲しますが、`svc.CreateUser()`は下のような実装になっていて、リポジトリの`CreateUser()`は1回しか呼び出されていません。なので、`Times(1)`と設定しています。

```go
func (s *UserService) CreateUser(name string) (*entity.User, error) {
	user, err := s.repo.CreateUser(name)
	if err != nil {
		return nil, err
	}
	return user, nil
}
```

:::note
#### `Times()`について
Times(1)と書いていますが、実はデフォルトで`Times(1)`となっているため、書かなくてもいいです。
しかし、Times()の引数の数字はもし1回以上呼ばれる場合、`Times()`もしくは`AnyTimes`(実行回数制限かけないメソッド)を使わないとエラーを吐いてしまうので、気をつけてください。
>Times()がデフォルトで1となっているのは、[MinTimes()](https://pkg.go.dev/go.uber.org/mock/gomock#Call.MinTimes)に下のような記述があります。
`MinTimes requires the call to occur at least n times. If AnyTimes or MaxTimes have not been called or if MaxTimes was previously called with 1, MinTimes also sets the maximum number of calls to infinity.`
:::