17
6

More than 3 years have passed since last update.

gomockのDoメソッド使えばモックに渡される引数の値をチェックするテストが書ける

Posted at

はじめに

gomockfunc (*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
}
17
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
6