前に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だとその辺の対応無しにシンプルにモックが行えて、良いツールだなと思いました。