はじめに
gomockのfunc (*Call) Doメソッドに関してネット上のサンプルコードが少ないという印象だったので本記事を書くことにしました。
環境
- go version go1.14.2 darwin/amd64
- github.com/golang/mock v1.4.3
コード例
やりたいこと: ユーザ登録をRepository層としてのインターフェースを通して実行するメソッドのテストを書く。
このとき、構造体の変換処理をするので変換後のDB側構造体が期待するものかをチェックしたい。
テスト対象のメソッド
app.go
package app
import (
"database/sql"
)
type IUserRepository interface {
InsertAUser(user *UserModel) error
}
type UserModel struct {
ID uint64 `db:"id"`
Name sql.NullString `db:"name"`
Email string `db:"email"`
}
type User struct {
UserID uint32 `json:"user_id"`
Name string `json:"user_name"`
Email string `json:"user_email"`
}
type userService struct {
userRepo IUserRepository
}
func (s *userService) RegisterUser(user *User) error {
var userModel UserModel
// Mapping process part
// ID
userModel.ID = uint64(user.UserID)
// Name
if user.Name == "" {
userModel.Name = sql.NullString{}
} else {
userModel.Name = sql.NullString{
String: user.Name,
Valid: true,
}
}
// Email
userModel.Email = user.Email
// Call repository func
err := s.userRepo.InsertAUser(&userModel) // この&userModelの内容のチェックをテストでしたい。
if err != nil {
return err
}
return nil
}
テストコード with gomock
gomockのfunc (*Call) Doメソッドを利用するとモックに渡される引数を取得できるのでRegisterUser
内でマッピングした結果のDB側構造体のチェックすることができます。
app_test.go
package app
import (
"testing"
"github.com/golang/mock/gomock"
)
//go:generate mockgen -source=app.go -destination=mock.go -package=app
func TestRegisterUser(t *testing.T) {
// Actual mapped UserModel
var actUserModel *UserModel
// Mock
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
userRepoMock := NewMockIUserRepository(mockCtrl)
userRepoMock.EXPECT().
InsertAUser(gomock.Any()).
Do(func(um *UserModel) {
// Do を使ってモック関数への引数を得ることができる。
// Do に渡す引数は`actUser`を持つクロージャ関数となる。
actUserModel = um
}).
Return(nil)
us := &userService{
userRepo: userRepoMock,
}
// Input
input := User{
UserID: 123,
Name: "John",
Email: "john@example.com",
}
// Act
_ = us.RegisterUser(&input)
// Assert to test mapping
// マッピング処理の結果をチェック
if uint32(actUserModel.ID) != input.UserID {
t.Error()
}
if name, _ := actUserModel.Name.Value(); name != input.Name {
t.Error()
}
if actUserModel.Email != input.Email {
t.Error()
}
}
テストコード内にgo:generate
を記述しているのでgo generate
でモックのファイルが生成されます。
$ go generate
$ ls mock.go
mock.go
テスト
$ go test -v
=== RUN TestRegisterUser
--- PASS: TestRegisterUser (0.00s)
PASS
ok github.com/momotaro98/go-codes-for-learning/3rd-parties/gomock 0.057s
補足 gomockのfunc (*Call) Do の実装を見てみる
gomock/call.go(筆者のコメント付き)
func (c *Call) Do(f interface{}) *Call {
// 引数関数のリフレクションして型を特定する。
v := reflect.ValueOf(f)
c.addAction(func(args []interface{}) []interface{} {
// テスト実行時に渡ってくる対象モックメソッドへの引数(`args`)も`reflect.Value`を得ることで型を特定する。
vargs := make([]reflect.Value, len(args))
ft := v.Type()
for i := 0; i < len(args); i++ {
if args[i] != nil {
vargs[i] = reflect.ValueOf(args[i])
} else {
// Use the zero value for the arg.
vargs[i] = reflect.Zero(ft.In(i))
}
}
// Doメソッドに渡した関数をテスト時に渡ってきた引数を渡して実行する。
// reflect.Valueにしているのでfが受ける引数の型とvargsの型が一致していなければpanicとなる。
v.Call(vargs)
return nil
})
return c
}