🥇
Goのコード生成を利用してユニットテストを書く方法
読んでわかること
入出力を保証するユニットテストのテンプレートを自動生成する方法。
私が考えるテストのテンプレートを自動生成メリット
- テストケースに集中できる。
- テストコードが統一される。
- テスト追加を忘れない。テストを書く手間が省ける。
テスト対象
前提として今回はユニットテストです。
引数がaのときの出力がbであることの保証をします。
internal/usecases/user_usecase.go
package usecases
import "sample/internal/models"
type (
UserUsecase struct {
userRepo UserRepo
}
UserRepo interface {
Find(id int) models.User
}
)
func NewUserUsecase(userRepo UserRepo) UserUsecase {
return UserUsecase{userRepo}
}
func (u UserUsecase) GetMe(id int) (models.User, error) {
return u.userRepo.Find(id), nil
}
環境
- macOSMonterey 12.2.1
- x86_64
- go1.18
準備
go get -u github.com/cweill/gotests/...
go install github.com/golang/mock/mockgen@latest
go install github.com/sanposhiho/gomockhandler@latest
テストコード生成
gotests -w -all internal/usecases/user_usecase.go
生成物
internal/usecases/user_usecase_test.go
package usecases
import (
"reflect"
"sample/internal/models"
"testing"
)
// Snip ...
func TestUserUsecase_GetMe(t *testing.T) {
type fields struct {
userRepo UserRepo
}
type args struct {
id int
}
tests := []struct {
name string
fields fields
args args
want models.User
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u := UserUsecase{
userRepo: tt.fields.userRepo,
}
got, err := u.GetMe(tt.args.id)
if (err != nil) != tt.wantErr {
t.Errorf("UserUsecase.GetMe() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("UserUsecase.GetMe() = %v, want %v", got, tt.want)
}
})
}
}
これでテストケース以外のものが生成されました。
具体的には、
- テスト対象の構造体の初期化
- テスト対象のメソッドの呼び出し
- errが返るべきかの確認
- メソッド呼び出し返り値と期待値のreflect.DeepEqualでの比較
後はテストケースだけ自分で記述すれば、ひとまずテストはできます。
何が足りないか
このままでも最低限は成立しますが、場合によって足りないものがあります
- モックコード
- モックのDI
- errが返ってほしいとき、具体的に何のエラーが返ったかのテスト
- 対象が状態を持つ場合、その状態のテスト
今回はモックコードの自動生成だけ扱います。
モックの生成
gomockhandler -config=gomockhandler.json -source=internal/usecases/user_usecase.go -destination=internal/mock_usecases/mock_user_usecase.go -package=mock_usecases
生成物
internal/mock_usecases/mock_user_usecase.go
// Code generated by MockGen. DO NOT EDIT.
// Source: internal/usecases/user_usecase.go
// Package mock_usecases is a generated GoMock package.
package mock_usecases
import (
reflect "reflect"
models "sample/internal/models"
gomock "github.com/golang/mock/gomock"
)
// MockUserRepo is a mock of UserRepo interface.
type MockUserRepo struct {
ctrl *gomock.Controller
recorder *MockUserRepoMockRecorder
}
// MockUserRepoMockRecorder is the mock recorder for MockUserRepo.
type MockUserRepoMockRecorder struct {
mock *MockUserRepo
}
// NewMockUserRepo creates a new mock instance.
func NewMockUserRepo(ctrl *gomock.Controller) *MockUserRepo {
mock := &MockUserRepo{ctrl: ctrl}
mock.recorder = &MockUserRepoMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockUserRepo) EXPECT() *MockUserRepoMockRecorder {
return m.recorder
}
// Find mocks base method.
func (m *MockUserRepo) Find(id int) models.User {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Find", id)
ret0, _ := ret[0].(models.User)
return ret0
}
// Find indicates an expected call of Find.
func (mr *MockUserRepoMockRecorder) Find(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockUserRepo)(nil).Find), id)
}
UserUsecaseが必要としている、UserRepoを自動生成してくれました。
NewMockUserRepo
関数を利用してモックを初期化しましょう
モックを使用したテスト
package usecases
import (
"reflect"
"sample/internal/mock_usecases"
"sample/internal/models"
"testing"
"github.com/golang/mock/gomock"
)
// Snip ...
var validUser = models.User{ID: 1, Name: "mock_return", Age: 19}
func TestUserUsecase_GetMe(t *testing.T) {
type fields struct {
userRepo UserRepo
}
type args struct {
id int
}
tests := []struct {
name string
args args
want models.User
wantErr bool
expectMockFn func(mockUserRepo *mock_usecases.MockUserRepo)
}{
{"case1", 1, validUser, false, func(mockUserRepo *mock_usecases.MockUserRepo) {
mockUserRepo.EXPECT().Find(1).Return(validUser)
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockUserRepo := mock_usecases.NewMockUserRepo(ctrl)
tt.expectMockFn(mockUserRepo)
u := UserUsecase{
userRepo: mockUserRepo,
}
got, err := u.GetMe(tt.args.id)
if (err != nil) != tt.wantErr {
t.Errorf("UserUsecase.GetMe() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("UserUsecase.GetMe() = %v, want %v", got, tt.want)
}
})
}
}
Go generateで生成できるように
go generate ./...
で全てのテストコードとモックを生成できるようにします。
$SAMPLE_REPO_ROOT
を定義して、gomockhandler.jsonをレポジトリのルートに指定しています。
internal/usecases/user_usecase.go
package usecases
//go:generate gotests -w -all $GOFILE
//go:generate gomockhandler -config=$SAMPLE_REPO_ROOT/gomockhandler.json -source=$GOFILE -destination=mock_$GOPACKAGE/mock_$GOFILE -package=mock_$GOPACKAGE
import "sample/internal/models"
type (
UserUsecase struct {
userRepo UserRepo
}
UserRepo interface {
Find(id int) models.User
}
)
func NewUserUsecase(userRepo UserRepo) UserUsecase {
return UserUsecase{userRepo}
}
func (u UserUsecase) GetMe(id int) (models.User, error) {
return u.userRepo.Find(id), nil
}
今後
- モックのDI
- gotestsをモックが使用できるように編集する必要がある。
- errが返ってほしいとき、具体的に何のエラーが返ったかのテスト
個人的には上記は解決したいと考えます。
モックのDIに関しては google/wire を使用して解決しようと思っています。
2,3 に関しては gotestsがテンプレートを設定できそうなので試してみようと思います。
次回はこれらを解消してまとめ版の記事としたいと思います。
Discussion