この記事は 弁護士ドットコム Advent Calendar 2022 の7日目の記事です。
はじめに
Go のテストにおいてインターフェースからモックを生成する gomock の基本的な使用からサンプルコードを使ってテストコードを書くところまでを説明します。
golang/mock とは
Go 公式が提供しているライブラリで、インターフェースの定義からモックを生成することができます。
- mockgen … モックを自動生成するツール
- gomock … 生成したモックを扱うパッケージ
インストール
README に記載されている通りにインストールしてください。
mockgen
というCLIツールと gomock
というパッケージの両方をインストールする必要があります。
-
mockgen (初回のみ)
$ go install github.com/golang/mock/mockgen
-
gomock
$ go get github.com/golang/mock/gomock
サンプルコード
サンプルとして、プレイヤー情報の一覧を外部のAPI から取得し、ランキング順に並び替えて返却する処理を作成します。
こちらの実装を gomock を使ってテストします。
※ 今回はインターフェース(外部のAPI)の実装は省略します。
.
├── model
│ └── player.go # プレイヤー情報のモデル
├── repository
│ └── player_api.go # プレイヤーAPIのインターフェースを定義
├── usecase
│ └── get_player_ranking.go # プレイヤー情報を取得し、ランキングを返す
└──go.mod
流れとしては、player_api.go
からインターフェースのモックを作成し、get_player_ranking.go
の実装をモックを使ってテストします。
下記は今回扱うプレイヤー情報のモデルです。
package model
type Player struct {
ID string
Name string
Ranking int
}
gomockの基本的な扱い方
モックの生成
下記のインターフェースを元にモックを作成します。
package repository
type PlayerAPIRepository interface {
GetPlayerList(context.Context) ([]*model.Player, error)
}
下記のようなコマンドを実行することでモックが生成されます。
$ mockgen -source=player_api.go -destination=./mock/player_api_mock.go -package=repository
-
-source
… モックを生成する対象のファイルを指定 -
-destination
… 生成したモックを保存する場所を指定 -
-package
… 生成するモックのパッケージ名を指定
コマンド実行後に自動でモックが生成されます。
├── repository
│ ├── mock
│ │ └── player_api_mock.go
│ └── player_api.go
下記は mockgen から自動生成されたソースコードです。
詳しくは後述しますので、読み飛ばしていただいて構いません。
// Code generated by MockGen. DO NOT EDIT.
// Source: player_api.go
// Package repository is a generated GoMock package.
package repository
import (
context "context"
reflect "reflect"
model "example.com/model"
gomock "github.com/golang/mock/gomock"
)
// MockPlayerAPIRepository is a mock of PlayerAPIRepository interface.
type MockPlayerAPIRepository struct {
ctrl *gomock.Controller
recorder *MockPlayerAPIRepositoryMockRecorder
}
// MockPlayerAPIRepositoryMockRecorder is the mock recorder for MockPlayerAPIRepository.
type MockPlayerAPIRepositoryMockRecorder struct {
mock *MockPlayerAPIRepository
}
// NewMockPlayerAPIRepository creates a new mock instance.
func NewMockPlayerAPIRepository(ctrl *gomock.Controller) *MockPlayerAPIRepository {
mock := &MockPlayerAPIRepository{ctrl: ctrl}
mock.recorder = &MockPlayerAPIRepositoryMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPlayerAPIRepository) EXPECT() *MockPlayerAPIRepositoryMockRecorder {
return m.recorder
}
// GetPlayerList mocks base method.
func (m *MockPlayerAPIRepository) GetPlayerList(arg0 context.Context) ([]*model.Player, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPlayerList", arg0)
ret0, _ := ret[0].([]*model.Player)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPlayerList indicates an expected call of GetPlayerList.
func (mr *MockPlayerAPIRepositoryMockRecorder) GetPlayerList(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlayerList", reflect.TypeOf((*MockPlayerAPIRepository)(nil).GetPlayerList), arg0)
}
生成されたモックを見てみる
生成されたモックを見てみます。
まずモックのファイルには2つの構造体が定義されています。
// MockPlayerAPIRepository is a mock of PlayerAPIRepository interface.
type MockPlayerAPIRepository struct {
ctrl *gomock.Controller
recorder *MockPlayerAPIRepositoryMockRecorder
}
// MockPlayerAPIRepositoryMockRecorder is the mock recorder for MockPlayerAPIRepository.
type MockPlayerAPIRepositoryMockRecorder struct {
mock *MockPlayerAPIRepository
}
-
MockPlayerAPIRepository
は、type PlayerAPIRepository interface
を満たす構造体です -
MockPlayerAPIRepositoryMockRecorder
は、モックの呼び出しなどを管理するための構造体で、利用者側では特に意識する必要はないです
// NewMockPlayerAPIRepository creates a new mock instance.
func NewMockPlayerAPIRepository(ctrl *gomock.Controller) *MockPlayerAPIRepository {
mock := &MockPlayerAPIRepository{ctrl: ctrl}
mock.recorder = &MockPlayerAPIRepositoryMockRecorder{mock}
return mock
}
-
NewMockPlayerAPIRepository
は、*gomock.Controller
からPlayerAPIRepository
のモックを生成します-
*gomock.Controller
の生成は後述します
-
- モックを使用する際は、こちらの関数を使用してください
モックを使ってテストを書いてみる
ここからは実際にサンプルコードの実装とテストを書いていきます。
下記は「プレイヤー情報の一覧を外部のAPI から取得し、ランキング順に並び替える」実装です。
package usecase
import (
"context"
"errors"
"sort"
"example.com/model"
"example.com/repository"
)
type GetPlayerRankingInputPort interface {
Execute(context.Context) (*GetPlayerRankingOutput, error)
}
type GetPlayerRankingOutput struct {
Ranking []*model.Player
Count int
}
type GetPlayerRankingInteractor struct {
PlayerAPIRepository repository.PlayerAPIRepository
}
func NewGetPlayerRankingUsecase(pr repository.PlayerAPIRepository) GetPlayerRankingInputPort {
return &GetPlayerRankingInteractor{
PlayerAPIRepository: pr,
}
}
func (i *GetPlayerRankingInteractor) Execute(ctx context.Context) (*GetPlayerRankingOutput, error) {
player, err := i.PlayerAPIRepository.GetPlayerList(ctx)
if err != nil {
return nil, err
}
if len(player) == 0 {
return nil, errors.New("not found")
}
sort.Slice(player, func(i, j int) bool {
return player[i].Ranking < player[j].Ranking
})
output := &GetPlayerRankingOutput{
Ranking: player,
Count: len(player),
}
return output, nil
}
次にテストを作成します。
テストファイル get_player_ranking_test.go
を作成し、テストを実装します。
package usecase_test
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"example.com/model"
repository "example.com/repository/mock"
"example.com/usecase"
)
func TestGetPlayerRankingInteractor_Execute(t *testing.T) {
ctx := context.Background()
resp := []*model.Player{
{ID: "1", Name: "test-player-1", Ranking: 3},
{ID: "2", Name: "test-player-2", Ranking: 2},
{ID: "3", Name: "test-player-3", Ranking: 1},
}
// (1) モックを呼び出すための Controller を生成
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// (2) モックの生成
pr := repository.NewMockPlayerAPIRepository(ctrl)
// (3) テストに呼ばれるべきメソッドと引数・戻り値を指定
pr.EXPECT().GetPlayerList(ctx).Return(resp, nil)
// (4) テストの本体
output, err := usecase.NewGetPlayerRankingUsecase(pr).Execute(ctx)
require.NoError(t, err)
require.Len(t, output.Ranking, 3)
for i, p := range output.Ranking {
require.Equal(t, i+1, p.Ranking)
}
require.Equal(t, 3, output.Count)
}
上記のテストで何を行ったのか解説します。
- モックを呼び出すための Controller を生成
- 最初に、
gomock.NewController(t)
で、モックを使用するための Controller を生成します - 後処理の
ctrl.Finish()
は、後述するモックが全て呼び出されたかどうかを確認しています
- 最初に、
- モックの生成
-
PlayerAPIRepository
インターフェースのモックを作成しています - 前述した
NewMockPlayerAPIRepository
関数でモックを作成します
-
- テストに呼ばれるべきメソッドと引数・戻り値を指定
-
EXPECT().Method()
により、テスト中に対象のメソッドが呼び出されることを期待します-
ctrl.Finish()
の後処理までに対応するメソッドが実行されないとエラーになります
つまり、EXPECT()
によってモックのメソッドが呼び出されたかどうかのテストができます
-
-
EXPECT()
は、*gomock.Call
を返します -
Return()
は、モックの呼び出しの戻り値が指定できます
-
- テストの本体
より複雑なテストを書いてみる
先ほどのサンプルより少し複雑なテストを書いていきます。
API によるプレイヤー情報の取得(GetPlayerList
メソッド)からエラーが返却された場合にリトライする処理を追加します。
func (i *GetPlayerRankingInteractor) Execute(ctx context.Context) (*GetPlayerRankingOutput, error) {
player, err := i.PlayerAPIRepository.GetPlayerList(ctx)
if err != nil {
// リトライする
player, err := i.PlayerAPIRepository.GetPlayerList(ctx)
if err != nil {
return nil, err
}
}
// 省略
return output, nil
}
gomock.InOrder
関数を使うことで、モックの呼び出し順序を指定することができます。
こちらを使うことで上記のようなリトライ処理のテストを書くことができます。
func TestGetPlayerRankingInteractor_Execute(t *testing.T) {
ctx := context.Background()
resp := []*model.Player{
{ID: "1", Name: "test-player-1", Ranking: 3},
{ID: "2", Name: "test-player-2", Ranking: 2},
{ID: "3", Name: "test-player-3", Ranking: 1},
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
pr := repository.NewMockPlayerAPIRepository(ctrl)
// モックが呼び出される順番を指定する
gomock.InOrder(
pr.EXPECT().GetPlayerList(ctx).Return(nil, errors.New("timeout")),
pr.EXPECT().GetPlayerList(ctx).Return(resp, nil),
)
output, err := usecase.NewGetPlayerRankingUsecase(pr).Execute(ctx)
require.NoError(t, err)
require.Len(t, output.Ranking, 3)
for i, p := range output.Ranking {
require.Equal(t, i+1, p.Ranking)
}
require.Equal(t, 3, output.Count)
}
上記のテストについて解説します。
-
gomock.InOrder
を使い、GetPlayerList
メソッドの呼び出し順を指定します- 1回目 … エラーを返却する
- 2回目 … 正常にプレイヤー情報の一覧を返却する
つまり、GetPlayerList
メソッドがエラーを返した際に、再度 GetPlayerList
メソッドが実行されないとテストは失敗します。
おまけ
その他にも以下のようなメソッド等を使うことでモックの呼び出しを指定することができます。
-
*gomock.Call.Times
メソッドで、メソッドの呼び出し回数が指定pr.EXPECT().GetPlayerList(ctx).Return(resp, nil).Times(2)
-
*gomock.Call.AnyTimes
メソッドで、メソッドの呼び出しが複数回呼べることを指定pr.EXPECT().GetPlayerList(ctx).Return(resp, nil).AnyTimes()
-
gomock.Any()
で、常に一致する引数を指定pr.EXPECT().Save(ctx, gomock.Any())
-
*gomock.Call.DoAndReturn
メソッドで、呼び出し後のアクションと戻り値を指定pr.EXPECT().Save(ctx, gomock.Any()). DoAndReturn(func(ctx context.Context, player *model.Player) error { require.Equal(t, "12345", player.ID) require.Equal(t, "test-player", player.Name) require.Equal(t, 1, player.Ranking) return nil })
さいごに
gomock の基本的な使用方法から複雑なテストコードを書くところまで説明しました。
外部のAPIやDBサーバへの接続を行う処理などを使ったテストでもモックを使うことで簡単に実装ができ、より複雑なテストが書けます。
ぜひ gomock を使ってテストを書いてみてください。
明日は @edi_t さんです。お楽しみに。