【小ネタ】gomockでカスタムMatcherを使う
2021-04-06

#要約
unittestやってますか?
unittestで便利なツール、gomockでカスタムMatcherを使って拡張する方法をご紹介します。
mockの引数チェックでgomock.Eq()
でチェックすると単純な比較となってしまい、動的なデータ、例えばタイムスタンプなどは比較対象外にしたいということが出来なくて困ったりします。
このようなユースケースでカスタムMatcherが役に立ちます。
#課題
次のようなコードがあるとします。
- interfaces/interfaces.go
package interfaces
import "time"
//go:generate go install github.com/golang/mock/mockgen
//go:generate mockgen -destination=${GOPACKAGE}mocks/${GOFILE} -package=${GOPACKAGE}mocks -source=$GOFILE
type IRepository interface {
Update(data *Data) error
}
type Data struct {
ID int
UpdatedAt time.Time
}
- repository/repository.go
package repository
import "github.com/cloudandbuild/blog-article-resrouces/content/blog/article-5/interfaces"
type ARespository struct {
interfaces.IRepository
}
func NewRepository() *ARespository {
return &ARespository{}
}
func (r *ARespository) Update(data *interfaces.Data) error {
// not implemented yet
return nil
}
- service/service.go
package service
import (
"time"
"github.com/cloudandbuild/blog-article-resrouces/content/blog/article-5/interfaces"
)
type AService struct {
ARepository interfaces.IRepository
}
func (r *AService) DoSomething() error {
d := &interfaces.Data{
ID: 1,
UpdatedAt: time.Now(),
}
return r.ARepository.Update(d)
}
処理の流れはService
が Repository
のUpdate
を呼び出すDoSomething
関数があります。
DoSomething関数がテスト対象です。このとき、Updateに渡すData
構造体のUpdatedAt
が現在時刻を指します。これをgomockでテストするとき、単純にgomock.Eq()で照合するとUpdatedAt
が合わなくて失敗してしまいます。
#カスタムMatcherを作る
そこで、gomockのカスタムMatcherを作って、テストごとにMatcherを定義するようにしたいと思います。
- testutil/testutil.go
gomockは、Matcher Interfaceを提供していますので、これを実装します。
テストごとにMatcherを定義できるよう比較関数を渡せるようにしています。
package testutil
import (
"fmt"
"github.com/golang/mock/gomock"
)
func NewMatcher(x interface{}, matchFn func(a, b interface{}) bool) gomock.Matcher {
return &Matcher{
x: x,
matchFn: matchFn,
}
}
type Matcher struct {
gomock.Matcher
x interface{}
matchFn func(a, b interface{}) bool
}
func (m *Matcher) Matches(x interface{}) bool {
return m.matchFn(m.x, x)
}
func (m *Matcher) String() string {
return fmt.Sprintf("is equal to %v", m.x)
}
- service/service_test.go
gomockのカスタムMatcherを使ったテストコードがこちらになります。
go-cmpのIgnoreFiledsでUpdatedAt
を無視するようにしました。
go-cmpだけでなく、自由にMatcherを実装することができるので自由度が広がると思います。
package service
import (
"testing"
"github.com/cloudandbuild/blog-article-resrouces/content/blog/article-5/interfaces"
"github.com/cloudandbuild/blog-article-resrouces/content/blog/article-5/interfaces/interfacesmocks"
"github.com/cloudandbuild/blog-article-resrouces/content/blog/article-5/testutil"
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func TestAService_DoSomething(t *testing.T) {
type fields struct {
ARepository func(ctrl *gomock.Controller) interfaces.IRepository
}
tests := []struct {
name string
fields fields
wantErr bool
}{
{
name: "success",
fields: fields{
ARepository: func(ctrl *gomock.Controller) interfaces.IRepository {
ret := interfacesmocks.NewMockIRepository(ctrl)
ret.EXPECT().Update(
testutil.NewMatcher(
&interfaces.Data{
ID: 1,
},
func(a, b interface{}) bool {
if diff := cmp.Diff(a, b, cmpopts.IgnoreFields(interfaces.Data{}, "UpdatedAt")); diff != "" {
t.Errorf("MockSomeRepository.Update mismatch (-want +got): %s", diff)
return false
}
return true
},
),
).Return(nil).Times(1)
return ret
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
r := &AService{
ARepository: tt.fields.ARepository(mockCtrl),
}
if err := r.DoSomething(); (err != nil) != tt.wantErr {
t.Errorf("AService.DoSomething() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
#テスト実行
go mod init
go mod tidy
go generate ./...
go test ./...
? github.com/cloudandbuild/blog-article-resrouces/content/blog/article-5/interfaces [no test files]
? github.com/cloudandbuild/blog-article-resrouces/content/blog/article-5/interfaces/interfacesmocks [no test files]
? github.com/cloudandbuild/blog-article-resrouces/content/blog/article-5/repository [no test files]
ok github.com/cloudandbuild/blog-article-resrouces/content/blog/article-5/service 0.003s
? github.com/cloudandbuild/blog-article-resrouces/content/blog/article-5/testutil [no test files]
#まとめ
今回はGoのgomockのカスタムMatcherをご紹介しました。
Goで開発し始めてからは、gomockをはじめとしてテストのツールが便利だったり書きやすいのでテストがはかどっています。単体テストあるのとないのでは、全然生産性違うのでぜひテスト筋力を鍛えて、テストカバー率を上げていきたいですね💪
全てのソースコードはこちら。
