Edited at

testify/mockでgolangのテストを書く


はじめに

testify

モックを使用したテストを書く上で、こちらのmockパッケージが非常に便利だったので記載しました。

テストのハードルを下げる足しになればと思います。

testifyは他にもアサーション、テスト・スイートの機能がありますが、

ここではモックについてのみ説明します。


簡単な使用例


前提条件


  • テスト時にはモックに置き換えられるようなつくり


テスト対象


  • テストの対象となるビジネスロジック

  • 本例ではDIによりモック対象を注入する

package service

type User struct {
dt datastore.UserInterface // モック対象
}

func (u *User) UserName(id int) (string, error) {
usr, err := u.dt.Get(id)
if usr == nil || err != nil {
return "", err
}
return usr.Name, nil
}


モック


  • テスト対象のテストを行うため下記を実施


    • テスト用の許容引数を設定

    • 戻り値を返す



  • テストダブルにおけてスパイ・スタブ・モックとあるうち、モックに当たる




ターゲットとなるinterface

package datastore

// UserInterface モック対象となるinterface
type UserInterface interface {
Get(id int) (*model.User, error)
}


モックstruct


  • mock.Mockを埋め込む

  • メソッド内でCalledメソッドを実行し、retを取得



  • ret.Get(戻り値のindex)をメソッドの戻り値とする


    • 戻り値がエラーの場合はret.Error(戻り値のindex)



import mock "github.com/stretchr/testify/mock"

// MockUserInterface UserInterfaceのモック
type MockUserInterface struct {
mock.Mock
}

// Get provides a mock function with given fields: id
func (_m *MockUserInterface) Get(id int) (*model.User, error) {
ret := _m.Called(id)
return ret.Get(0).(*model.User), ret.Error(1)
}


テストの実行


  1. モックの戻り値を設定

  2. テスト対象にモックを注入

  3. テスト実行(内部でモック実行)

func TestUser_UserName(t *testing.T) {

testUser := &model.User{ID: 1, Name: "Tom", Gender: model.Male, CreatedAt: time.Now(), UpdatedAt: time.Now()}

// モック
mockUser := new(datastore.MockUserInterface)
// モックの戻り値を設定
mockUser.On("Get", testUser.ID).Return(testUser, nil)

// テスト対象(モックを注入)
u := &User{
dt: mockUser,
}
// テスト実行(内部でモック実行)
got, err := u.UserName(testUser.ID)
if err != nil {
t.Error(err)
}
}


内部処理の説明

下記で要素で構成されています


  • Mock

  • Call

  • Arguments


Mock

https://godoc.org/github.com/stretchr/testify/mock#Mock

簡単に行ってしまえば下記を担っています


  1. Onメソッドでモックのメソッドに対する引数と戻り値を設定

  2. Calledメソッドで設定されたメソッド・引数に対する戻り値を返す


Mock.On メソッド

func (m *Mock) On(methodName string, arguments ...interface{}) *Call


  • あるモックのメソッドに対する許容する引数と戻り値の設定を行うためのメソッド


    • 厳密には引数に対する戻り値を設定するReturn情報を保持するためのCallインスタンスを返し、内部でプーリング。



  • モックの同一メソッドに対して複数Call設定可能


Mock.Called メソッド

func (m *Mock) Called(arguments ...interface{}) Arguments


  • 実行時の引数とOnメソッドで予め設定されていたCallの条件にあっているかを精査する

  • 条件にあったReturn設定値を返す


内部処理


  1. 実行されているメソッドと引数に対して、予め設定された許容値内かを精査する

  2. 許容範囲外であればエラーを返す


  3. Runメソッドが設定されていればその設定処理を実行。(戻り値には影響を及ぼさない)

  4. 待機時間が設定されていれば待機。(下記のどちらかが設定されていた場合)


    1. WaitUntilメソッド

    2. Afterメソッド



  5. 条件にあったReturn設定値を返す


Call

https://godoc.org/github.com/stretchr/testify/mock#Call


  • Callは開発者が設定した内容を保持

  • 構成要素


    • 引数(Arguments)

    • 戻り値

    • メソッド名

    • 繰り返し実行回数(0設定は無限)

    • 戻り値を返すまでの待機時間(未設定は即時)




Arguments

https://godoc.org/github.com/stretchr/testify/mock#Arguments


  • モックのメソッドの引数のスライス

  • 実行メソッドの引数の型としても利用される

  • 戻り値の型としても利用される


使用方法


メソッドの引数関連


  • 設定した引数で実行されたときのみ所定の戻り値を返す

  • 未設定の引数で実行するとエラーになる

mockUser.On("GetByNameAndGender", "Jane", model.Female).Return(usr1, nil)

usra, err := mockUser.GetByNameAndGender("Tom", model.Male)
// usra == usr

usra1, err := mockUser.GetByNameAndGender("Jane", model.Female)
// usra1 == usr1

usra2, err := mockUser.GetByNameAndGender("Bob", model.Male)
// NG(内部でエラー発生)


mock.Anything

値、型関係なくすべての引数に対して有効です

// すべての引数でusr, nilを返す

mockUser.On("GetByNameAndGender", mock.Anything, mock.Anything).Return(usr, nil)


mock.AnythingOfType

指定の型の引数全てに対して有効です

// すべての引数でusr, nilを返す

mockUser.On("GetByNameAndGender", mock.AnythingOfType("string"), mock.AnythingOfType("model.Gender")).Return(usr, nil)


mock.MatchedBy

引数の許容条件はfuncで判定します

// 引数が"Tom", model.Maleのときのみ有効

mockUser.On("GetByNameAndGender",
mock.MatchedBy(func(name string) bool { return name == "Tom" }),
mock.MatchedBy(func(gender model.Gender) bool { return gender == model.Male }),
).Return(usr)


有効実行回数関連


Onceメソッド


  • 1回だけ有効

  • それ以降の実行時にはたとえ引数が許容条件内でも無効となる

Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Once()

下記のようなケースで利用できるのではないでしょうか

mockUser.On("Get", 1).Return(nil, nil).Once() // 初回はnilを返す

mockUser.On("Insert", testUser.ID, param).Return(nil) // insert処理 (errorなし)
mockUser.On("Get", 1).Return(usr, nil) // 2回目以降はusrを返す
mockUser.On("Update", testUser.ID, param).Return(nil) // update処理 (errorなし)

// テスト実行
got, err := u.CreateOrUpdate(testUser.ID, param)


Twiceメソッド


  • 2回だけ有効

  • それ以外はOnceと同じ

Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Twice()


Times(n)メソッド


  • n回有効

  • それ以外はOnceと同じ

Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Times(n)


参照型引数により値を取得する


Runメソッド


  • モックのメソッド内で参照型引数の変更があるようなケースで使用

  • 実際に実行時に入力された引数の評価が終わったあとで、Returnによる返却が行われる前にRun内のfuncが実行される

mockUser.On("Assimilate", mock.AnythingOfType("int"), mock.AnythingOfType("*model.User")).Return(true, nil).Run(

func(args mock.Arguments) {
args[1] = userMap[args.Get(0).(int)]
// 第二引数に*model.Userインスタンスを代入
},
)

var mu model.User
res, err := mockUser.Assimilate(1, &mu)
// mu == *userMap[1]


戻り値を戻すまで待機


WaitUntilメソッド


  • 待機時間後にMockのReturnが実施されるような設定を行う

  • 引数をtime.Timeのchannelを取る

例えば下記のようなケースで使用可能ではないでしょうか


モック対象の元々のメソッド


  • selectによるタイムアウトを設定がある

func (u *user) SearchFriends(id int) ([]*model.User, error) {

cm := make(chan []*model.User)
ce := make(chan error)

go func() {
fs, err := u.searchSNSFriends(id)
if err != nil {
ce <- err
}
cm <- fs
}()

select {
case fs := <-cm:
return fs, nil
case err := <-ce:
return nil, err
case <-time.After(1 * time.Second):
return nil, fmt.Errorf("timeout")
}
}


テスト

mockUser.On("SearchFriends", usr.ID).Return(nil, err).WaitUntil(time.After(3 * time.Second))

usrs, err := mockUser.SearchFriends(usr.ID)
// 3秒後にerrorが返る


Afterメソッド


  • WaitUntil同様、待機時間後にMockのReturnが実施されるような設定を行う

  • 引数にtime.Durationを取る

mockUser.On("SearchFriends", usr.ID).Return(nil, err).After(3 * time.Second)

usrs, err := mockUser.SearchFriends(usr.ID)
// 3秒後にerrorが返る


その他


引数による戻り値の振り分け(応用)


  • モックのReturnでfuncも受け付けるように変更

  • わかりやすさのため、戻り値は一つのGetSimpleを使用

// モックのGetSimpleメソッド

func (_m *MockUserInterface) GetSimple(id int) *model.User {
ret := _m.Called(id)

var r0 *model.User
if rf, ok := ret.Get(0).(func(int) *model.User); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}

return r0
}

Returnの戻り値を引数に紐づけた利用が可能

mockUser.On("GetSimple", mock.Anything).Return(func(id int) *model.User {

var usr *model.User
var ok bool
if usr, ok = userMap[id]; !ok {
panic(fmt.Errorf("invalid id: %d", id))
}
return usr
})

usra2 := mockUser.GetSimple(2)
// usra2.ID == 2


モックの自動生成

こちらを使用すると、モック(Mockを埋め込んだstruct)を自動生成してくれます。

https://github.com/vektra/mockery

こちらで使用したモックは上記を使って作成しました。


最後に

一通り理解すると、かなりのケースを網羅できると思います。

また自動生成ツールを組み合わせると、スピーディーに書けるようになりました。