【小ネタ】gomockでカスタムMatcherを使う

要約
unittestやってますか?
unittestで便利なツール、gomockでカスタムMatcherを使って拡張する方法をご紹介します。
mockの引数チェックでgomock.Eq()
でチェックすると単純な比較となってしまい、動的なデータ、例えばタイムスタンプなどは比較対象外にしたいということが出来なくて困ったりします。
このようなユースケースでカスタムMatcherが役に立ちます。
課題
次のようなコードがあるとします。
- interfaces/interfaces.go
package interfacesimport "time"//go:generate go install github.com/golang/mock/mockgen//go:generate mockgen -destination=${GOPACKAGE}mocks/${GOFILE} -package=${GOPACKAGE}mocks -source=$GOFILEtype IRepository interface { Update(data *Data) error}type Data struct { ID int UpdatedAt time.Time}
- repository/repository.go
package repositoryimport "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 serviceimport ( "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 testutilimport ( "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 serviceimport ( "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 initgo mod tidygo 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をはじめとしてテストのツールが便利だったり書きやすいのでテストがはかどっています。単体テストあるのとないのでは、全然生産性違うのでぜひテスト筋力を鍛えて、テストカバー率を上げていきたいですね💪
全てのソースコードはこちら。
