前にGoMockを使ったテストコードについて記事にした事がありました。
当時はGoMockしか知りませんでしたが、moqというライブラリの方が簡単に使えるという話を記事をみかけたので、memoirではmoqを使用してGo言語のテストを書いてみました。
Pull Request
実装
まず、以下でコマンドをインストールする
$ go get github.com/matryer/moq
次にモックさせたいInterfaceを指定して、モックを作成します。 今回だと、テスト時にはfirestoreのアクセス部分をモックさせたかったので、以下のInterfaceを指定
package repository import ( "context" "time" "cloud.google.com/go/firestore" "github.com/wheatandcat/memoir-backend/graph/model" ) // UserRepositoryInterface is repository interface type ItemRepositoryInterface interface { Create(ctx context.Context, f *firestore.Client, userID string, i *model.Item) error Update(ctx context.Context, f *firestore.Client, userID string, i *model.UpdateItem, updatedAt time.Time) error Delete(ctx context.Context, f *firestore.Client, userID string, i *model.DeleteItem) error GetItem(ctx context.Context, f *firestore.Client, userID string, id string) (*model.Item, error) GetItemsByDate(ctx context.Context, f *firestore.Client, userID string, date time.Time) ([]*model.Item, error) }
モック作成では以下のようにInterfaceを指定します。
$ moq -out=repository/moq.go ./repository ItemRepositoryInterface
これで自生成されるファイルが以下の通りです。 ■ repository/moq.go
package repository import ( "cloud.google.com/go/firestore" "context" "github.com/wheatandcat/memoir-backend/graph/model" "sync" "time" ) // Ensure, that ItemRepositoryInterfaceMock does implement ItemRepositoryInterface. // If this is not the case, regenerate this file with moq. var _ ItemRepositoryInterface = &ItemRepositoryInterfaceMock{} // ItemRepositoryInterfaceMock is a mock implementation of ItemRepositoryInterface. // // func TestSomethingThatUsesItemRepositoryInterface(t *testing.T) { // // // make and configure a mocked ItemRepositoryInterface // mockedItemRepositoryInterface := &ItemRepositoryInterfaceMock{ // CreateFunc: func(ctx context.Context, f *firestore.Client, userID string, i *model.Item) error { // panic("mock out the Create method") // }, // DeleteFunc: func(ctx context.Context, f *firestore.Client, userID string, i *model.DeleteItem) error { // panic("mock out the Delete method") // }, // GetItemFunc: func(ctx context.Context, f *firestore.Client, userID string, id string) (*model.Item, error) { // panic("mock out the GetItem method") // }, // GetItemsByDateFunc: func(ctx context.Context, f *firestore.Client, userID string, date time.Time) ([]*model.Item, error) { // panic("mock out the GetItemsByDate method") // }, // UpdateFunc: func(ctx context.Context, f *firestore.Client, userID string, i *model.UpdateItem, updatedAt time.Time) error { // panic("mock out the Update method") // }, // } // // // use mockedItemRepositoryInterface in code that requires ItemRepositoryInterface // // and then make assertions. // // } type ItemRepositoryInterfaceMock struct { // CreateFunc mocks the Create method. CreateFunc func(ctx context.Context, f *firestore.Client, userID string, i *model.Item) error // DeleteFunc mocks the Delete method. DeleteFunc func(ctx context.Context, f *firestore.Client, userID string, i *model.DeleteItem) error // GetItemFunc mocks the GetItem method. GetItemFunc func(ctx context.Context, f *firestore.Client, userID string, id string) (*model.Item, error) // GetItemsByDateFunc mocks the GetItemsByDate method. GetItemsByDateFunc func(ctx context.Context, f *firestore.Client, userID string, date time.Time) ([]*model.Item, error) // UpdateFunc mocks the Update method. UpdateFunc func(ctx context.Context, f *firestore.Client, userID string, i *model.UpdateItem, updatedAt time.Time) error // calls tracks calls to the methods. calls struct { // Create holds details about calls to the Create method. Create []struct { // Ctx is the ctx argument value. Ctx context.Context // F is the f argument value. F *firestore.Client // UserID is the userID argument value. UserID string // I is the i argument value. I *model.Item } // Delete holds details about calls to the Delete method. Delete []struct { // Ctx is the ctx argument value. Ctx context.Context // F is the f argument value. F *firestore.Client // UserID is the userID argument value. UserID string // I is the i argument value. I *model.DeleteItem } // GetItem holds details about calls to the GetItem method. GetItem []struct { // Ctx is the ctx argument value. Ctx context.Context // F is the f argument value. F *firestore.Client // UserID is the userID argument value. UserID string // ID is the id argument value. ID string } // GetItemsByDate holds details about calls to the GetItemsByDate method. GetItemsByDate []struct { // Ctx is the ctx argument value. Ctx context.Context // F is the f argument value. F *firestore.Client // UserID is the userID argument value. UserID string // Date is the date argument value. Date time.Time } // Update holds details about calls to the Update method. Update []struct { // Ctx is the ctx argument value. Ctx context.Context // F is the f argument value. F *firestore.Client // UserID is the userID argument value. UserID string // I is the i argument value. I *model.UpdateItem // UpdatedAt is the updatedAt argument value. UpdatedAt time.Time } } lockCreate sync.RWMutex lockDelete sync.RWMutex lockGetItem sync.RWMutex lockGetItemsByDate sync.RWMutex lockUpdate sync.RWMutex } // Create calls CreateFunc. func (mock *ItemRepositoryInterfaceMock) Create(ctx context.Context, f *firestore.Client, userID string, i *model.Item) error { if mock.CreateFunc == nil { panic("ItemRepositoryInterfaceMock.CreateFunc: method is nil but ItemRepositoryInterface.Create was just called") } callInfo := struct { Ctx context.Context F *firestore.Client UserID string I *model.Item }{ Ctx: ctx, F: f, UserID: userID, I: i, } mock.lockCreate.Lock() mock.calls.Create = append(mock.calls.Create, callInfo) mock.lockCreate.Unlock() return mock.CreateFunc(ctx, f, userID, i) } // CreateCalls gets all the calls that were made to Create. // Check the length with: // len(mockedItemRepositoryInterface.CreateCalls()) func (mock *ItemRepositoryInterfaceMock) CreateCalls() []struct { Ctx context.Context F *firestore.Client UserID string I *model.Item } { var calls []struct { Ctx context.Context F *firestore.Client UserID string I *model.Item } mock.lockCreate.RLock() calls = mock.calls.Create mock.lockCreate.RUnlock() return calls } // Delete calls DeleteFunc. func (mock *ItemRepositoryInterfaceMock) Delete(ctx context.Context, f *firestore.Client, userID string, i *model.DeleteItem) error { if mock.DeleteFunc == nil { panic("ItemRepositoryInterfaceMock.DeleteFunc: method is nil but ItemRepositoryInterface.Delete was just called") } callInfo := struct { Ctx context.Context F *firestore.Client UserID string I *model.DeleteItem }{ Ctx: ctx, F: f, UserID: userID, I: i, } mock.lockDelete.Lock() mock.calls.Delete = append(mock.calls.Delete, callInfo) mock.lockDelete.Unlock() return mock.DeleteFunc(ctx, f, userID, i) } // DeleteCalls gets all the calls that were made to Delete. // Check the length with: // len(mockedItemRepositoryInterface.DeleteCalls()) func (mock *ItemRepositoryInterfaceMock) DeleteCalls() []struct { Ctx context.Context F *firestore.Client UserID string I *model.DeleteItem } { var calls []struct { Ctx context.Context F *firestore.Client UserID string I *model.DeleteItem } mock.lockDelete.RLock() calls = mock.calls.Delete mock.lockDelete.RUnlock() return calls } // GetItem calls GetItemFunc. func (mock *ItemRepositoryInterfaceMock) GetItem(ctx context.Context, f *firestore.Client, userID string, id string) (*model.Item, error) { if mock.GetItemFunc == nil { panic("ItemRepositoryInterfaceMock.GetItemFunc: method is nil but ItemRepositoryInterface.GetItem was just called") } callInfo := struct { Ctx context.Context F *firestore.Client UserID string ID string }{ Ctx: ctx, F: f, UserID: userID, ID: id, } mock.lockGetItem.Lock() mock.calls.GetItem = append(mock.calls.GetItem, callInfo) mock.lockGetItem.Unlock() return mock.GetItemFunc(ctx, f, userID, id) } // GetItemCalls gets all the calls that were made to GetItem. // Check the length with: // len(mockedItemRepositoryInterface.GetItemCalls()) func (mock *ItemRepositoryInterfaceMock) GetItemCalls() []struct { Ctx context.Context F *firestore.Client UserID string ID string } { var calls []struct { Ctx context.Context F *firestore.Client UserID string ID string } mock.lockGetItem.RLock() calls = mock.calls.GetItem mock.lockGetItem.RUnlock() return calls } // GetItemsByDate calls GetItemsByDateFunc. func (mock *ItemRepositoryInterfaceMock) GetItemsByDate(ctx context.Context, f *firestore.Client, userID string, date time.Time) ([]*model.Item, error) { if mock.GetItemsByDateFunc == nil { panic("ItemRepositoryInterfaceMock.GetItemsByDateFunc: method is nil but ItemRepositoryInterface.GetItemsByDate was just called") } callInfo := struct { Ctx context.Context F *firestore.Client UserID string Date time.Time }{ Ctx: ctx, F: f, UserID: userID, Date: date, } mock.lockGetItemsByDate.Lock() mock.calls.GetItemsByDate = append(mock.calls.GetItemsByDate, callInfo) mock.lockGetItemsByDate.Unlock() return mock.GetItemsByDateFunc(ctx, f, userID, date) } // GetItemsByDateCalls gets all the calls that were made to GetItemsByDate. // Check the length with: // len(mockedItemRepositoryInterface.GetItemsByDateCalls()) func (mock *ItemRepositoryInterfaceMock) GetItemsByDateCalls() []struct { Ctx context.Context F *firestore.Client UserID string Date time.Time } { var calls []struct { Ctx context.Context F *firestore.Client UserID string Date time.Time } mock.lockGetItemsByDate.RLock() calls = mock.calls.GetItemsByDate mock.lockGetItemsByDate.RUnlock() return calls } // Update calls UpdateFunc. func (mock *ItemRepositoryInterfaceMock) Update(ctx context.Context, f *firestore.Client, userID string, i *model.UpdateItem, updatedAt time.Time) error { if mock.UpdateFunc == nil { panic("ItemRepositoryInterfaceMock.UpdateFunc: method is nil but ItemRepositoryInterface.Update was just called") } callInfo := struct { Ctx context.Context F *firestore.Client UserID string I *model.UpdateItem UpdatedAt time.Time }{ Ctx: ctx, F: f, UserID: userID, I: i, UpdatedAt: updatedAt, } mock.lockUpdate.Lock() mock.calls.Update = append(mock.calls.Update, callInfo) mock.lockUpdate.Unlock() return mock.UpdateFunc(ctx, f, userID, i, updatedAt) } // UpdateCalls gets all the calls that were made to Update. // Check the length with: // len(mockedItemRepositoryInterface.UpdateCalls()) func (mock *ItemRepositoryInterfaceMock) UpdateCalls() []struct { Ctx context.Context F *firestore.Client UserID string I *model.UpdateItem UpdatedAt time.Time } { var calls []struct { Ctx context.Context F *firestore.Client UserID string I *model.UpdateItem UpdatedAt time.Time } mock.lockUpdate.RLock() calls = mock.calls.Update mock.lockUpdate.RUnlock() return calls }
自動生成されるファイルは大きいですが、使用方法は単純でテスト時に使用箇所を上記のInterfaceで置き換えてればOKです
package graph_test import ( "github.com/wheatandcat/memoir-backend/client/timegen" "github.com/wheatandcat/memoir-backend/client/uuidgen" "github.com/wheatandcat/memoir-backend/graph" "github.com/wheatandcat/memoir-backend/repository" ) func newGraph() graph.Graph { client := &graph.Client{ UUID: &uuidgen.UUID{}, Time: &timegen.Time{}, } app := &graph.Application{ ItemRepository: &repository.ItemRepositoryInterfaceMock{}, // ←ここにMockを設定 }
上記をテスト実行時に使用しつつ、必要な箇所のみ以下のように置き換えればテストが行えます
package graph_test import ( "context" "testing" "time" "cloud.google.com/go/firestore" "github.com/google/go-cmp/cmp" "github.com/wheatandcat/memoir-backend/client/timegen" "github.com/wheatandcat/memoir-backend/client/uuidgen" "github.com/wheatandcat/memoir-backend/graph" "github.com/wheatandcat/memoir-backend/graph/model" "github.com/wheatandcat/memoir-backend/repository" "gopkg.in/go-playground/assert.v1" ) func TestGetItemsByDate(t *testing.T) { ctx := context.Background() client := &graph.Client{ UUID: &uuidgen.UUID{}, Time: &timegen.Time{}, } date := client.Time.ParseInLocation("2019-01-01T00:00:00") items := []*model.Item{{ ID: "test1", CategoryID: 1, Title: "test-title", Date: date, CreatedAt: date, UpdatedAt: date, }} g := newGraph() // テストでGetItemsByDateを使用するので↓のみreturnを設定する itemRepositoryMock := &repository.ItemRepositoryInterfaceMock{ GetItemsByDateFunc: func(ctx context.Context, f *firestore.Client, userID string, date time.Time) ([]*model.Item, error) { return items, nil }, } g.App.ItemRepository = itemRepositoryMock tests := []struct { name string param time.Time result []*model.Item }{ { name: "日付でアイテムを取得する", param: date, result: items, }, } for _, td := range tests { t.Run(td.name, func(t *testing.T) { r, _ := g.GetItemsByDate(ctx, td.param) diff := cmp.Diff(r, td.result) if diff != "" { t.Errorf("differs: (-got +want)\n%s", diff) } else { assert.Equal(t, diff, "") } }) } }
これでテスト実行で成功しました。
$ go test ./graph ok github.com/wheatandcat/memoir-backend/graph
GoMockで実装していた時はメソッドの引数の辻褄が合わないとか、テストの本質じゃないところで詰まることが多かったが、moqだとその辺の対応無しにシンプルにモックが行えて、良いツールだなと思いました。