はじめに
Go言語を用いた開発においては、コードの品質担保や予期せぬ修正の影響による不具合検知などのために、標準機能の go test で実行できる単体テストをあわせて書くことが多いと思います。
単体テスト実装に際しては、並列実行で短時間にテストを終わらせるためだったり、GitHub Actions や CircleCI のような限定的な環境で実行できるよう、ファイルの入出力や外部のAPIやDBサーバへの接続を行う処理などを、モックで代用して記述することが多いと思います。
インターフェイス定義などからモック処理を自動で生成してくれるツール
GoMock の導入と 生成したモックを用いたテストの書き方の紹介です
前提
以下の環境、知識を前提とします
- go 1.16以降 Go Modules を利用
- Go言語の基礎知識
- go test の基礎知識
テスト対象のサンプルコード
※以下サンプルコードではプロジェクトのリポジトリURLを example.com/gotest (go mod init example.com/gotest
を実行した)と仮定しています。適宜読み替えてください。
package entity
// Foo entity
type Foo struct {
ID int64
Value string
}
package repository
import (
"example.com/gotest/entity"
)
type FooRepository interface {
FindByID(id int64) (*entity.Foo, error)
Insert(foo *entity.Foo) error
Update(foo *entity.Foo) error
}
実装は省略しています
package usecase
import (
"example.com/gotest/entity"
"example.com/gotest/repository"
)
type fooUseCase struct {
fooRepo repository.FooRepository
}
func (p *fooUseCase) Save(id int64, value string) (*entity.Foo, error) {
foo, err := p.fooRepo.FindByID(id)
if err != nil {
// 何らかのエラー
return nil, err
}
if foo != nil {
// 既存
foo.Value = value
if err := p.fooRepo.Update(foo); err != nil {
return nil, err
}
} else {
// 新規作成
foo = &entity.Foo{ID: id, Value: value}
if err := p.fooRepo.Insert(foo); err != nil {
return nil, err
}
}
// 再取得
return p.fooRepo.FindByID(id)
}
モック生成
mockgen CLIのインストール (初回のみ)
モックを生成するCLIをインストールします
go install github.com/golang/mock/mockgen@v1.6.0
モックコードの生成
以下の例では repository.FooRepository インターフェイスの実装のモックを生成させます
mockgen \
-source repository/foo.go \
-destination repository/mock/foo.go \
-package mock
-source
でモック化対象のソースファイルを指定
-destination
で生成したモックを保存するソースファイル名、
-package
で生成するモックのパッケージ名を指定
テストの実装
package usecase
import (
"reflect"
"testing"
"github.com/golang/mock/gomock"
"example.com/gotest/entity"
"example.com/gotest/repository/mock"
)
// TestFooUseCase_Save_NotExistsSuccess 新規保存のテストケース
func TestFooUseCase_Save_NotExistsSuccess(t *testing.T) {
// 期待値
testID := int64(123)
testValue := "hoge"
expectedSaved := &entity.Foo{
ID: testID,
Value: testValue,
}
// モックセット
ctrl := gomock.NewController(t)
defer ctrl.Finish()
fooRepoMock := mock.NewMockFooRepository(ctrl)
fooRepoMock.EXPECT().FindByID(gomock.Eq(testID)).Return(nil, nil).Times(1)
fooRepoMock.EXPECT().Insert(gomock.Eq(expectedSaved)).Return(nil)
fooRepoMock.EXPECT().FindByID(gomock.Eq(testID)).Return(expectedSaved, nil).Times(1)
// モックを注入して作成
u := &fooUseCase{fooRepo: fooRepoMock}
// 実行
actualSaved, err := u.Save(testID, testValue)
// 結果確認
if err != nil {
t.Errorf("予期せぬエラーが発生 %v", err)
}
if !reflect.DeepEqual(actualSaved, expectedSaved) {
t.Errorf("保存結果が期待値と異なる\n期待:%+v\n実際:%+v", expectedSaved, actualSaved)
}
}
補足
import
gomockパッケージ "github.com/golang/mock/gomock" と
上記で生成したモックパッケージ "example.com/gotest/repository/mock" をimport
モック関数生成と期待値のセット
各テスト関数では gomock.NewController を呼び出してモックコントローラを初期化し、
戻り値(ctrl)を各モックの New関数 NewMockモック化オブジェクト名(ctrl)
に渡してやる必要があります。
呼び出されることが期待されるすべてのモックの関数の引数の期待値(マッチ条件)と戻り値を設定する必要があります。
モックオブジェクト.EXPECT().呼び出される関数(1つ目の引数の期待値, 2つ目の引数の期待値...).Return(モック関数が返す戻り値)
.Times(回数)
は呼び出される回数を限定、
同じモック関数が複数呼び出されるようなケースでは、
モック関数の呼び出し順序を厳密に指定するgomock.InOrder関数で囲んだり、
.Return(戻り値)
の代わりに.DoAndReturn(戻り値を返す関数)で引数に応じた戻り値を返すことなどができます
テスト実行
$ go test -v --cover ./...
=== RUN TestFooUseCase_Save_NotExistsSuccess
--- PASS: TestFooUseCase_Save_NotExistsSuccess (0.00s)
PASS
coverage: 50.0% of statements
ok example.com/gotest/usecase 0.264s coverage: 50.0% of statements
無事に実行できました