1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

めんどくさめのinterfaceを満たすgoのmockライブラリを選定する

Posted at

今回は、

  • interfaceがinterfaceを返す
  • 返り値がメソッドチェーン的に自身を返す

...ような若干めんどくさいタイプのinterfaceをmockすることにする。

mockしたい仕様

base.go
package example

import "context"

type SampleRepository interface {
	Save(ctx context.Context, sample Sample) error
	FindByID(ctx context.Context)
	Where() SampleRepositorySearchSpec
}

type SampleRepositorySearchSpec interface {
	IDIn(id ...int) SampleRepositorySearchSpec
	NameLike(name string) SampleRepositorySearchSpec
	Find(ctx context.Context) (SampleList, error)
	Get(ctx context.Context) (*Sample, error)
}

type Sample struct {
	ID   int
	UUID string
}

type SampleList []Sample

  • SampleRepositoryWhere()SampleRepositorySearchSpec という新たなinterfaceを返すところがめんどくささの1。

  • SampleRepositorySearchSpec は Find() Get() が実行されるまで自分自身を返し続けることを求められるのがめんどくささの2です。

github.com/matryer/moq

生成

moq -out mocks_repository.go . SampleRepository SampleRepositorySearchSpec
  • 割と自分で書いたときのような見慣れた感じのシンプルなモックを吐く。
  • 以下のような感じのをジェネる
mocks_repository.go

type SampleRepositoryMock struct {
	// SaveFunc mocks the Save method.
	SaveFunc func(ctx context.Context, sample Sample) error

	// WhereFunc mocks the Where method.
	WhereFunc func() SampleRepositorySearchSpec

	// calls tracks calls to the methods.
	calls struct {
		// Save holds details about calls to the Save method.
		Save []struct {
			// Ctx is the ctx argument value.
			Ctx context.Context
			// Sample is the sample argument value.
			Sample Sample
		}
		// Where holds details about calls to the Where method.
		Where []struct {
		}
	}
}

....

mockするときは、メソッド名 + Funcみたいなメソッドが生えるので、
こいつに自由な値を返す関数を設定する、といった感じだ。

使用感

func TestExample(tt *testing.T) {

	// mock準備
	searcherMock := SampleRepositorySearchSpecMock{
		GetFunc: func(ctx context.Context) (sample *Sample, err error) {
			return &Sample{
				ID:   122,
				UUID: "xxxxx",
			}, nil
		},
		FindFunc: func(ctx context.Context) (list SampleList, err error) {
			return SampleList{
				{
					ID:   122,
					UUID: "xxxxx",
				},
				{
					ID:   123,
					UUID: "xxxxy",
				},
			}, nil
		},
	}

	searcherMock.IDInFunc = func(id ...int) SampleRepositorySearchSpec {
		return &searcherMock
	}

	repoMock := SampleRepositoryMock{
		SaveFunc: func(ctx context.Context, sample Sample) error {
			return nil
		},
		WhereFunc: func() SampleRepositorySearchSpec {
			return &searcherMock
		},
	}

	// テスト
	tt.Run(`subtest01`, func(t *testing.T) {
		sample122, err := repoMock.Where().IDIn(122).Get(context.Background())
		assert.NoError(t, err)
		assert.Equal(t, 122, sample122.ID)
	})

}

SampleRepositorySearchSpec 側のIDIn NameLike メソッドは自身を返さなければならないため、

    searcherMock.IDInFunc = func(id ...int) SampleRepositorySearchSpec {
        return &searcherMock
    }

のように外出ししないといけない。

github.com/golang/mock

 mockgen -source ./base.go  -package example  -destination example.mock.repository.go

割と短いコードが生成される

example.mock.repository.go

// Code generated by MockGen. DO NOT EDIT.
// Source: ./base.go

// Package mock_example is a generated GoMock package.
package example

import (
	example "bitbucket.org/wanocoltd/vktc/vkproj/videokicks/core/pkg/domain/domain_service/example"
	context "context"
	gomock "github.com/golang/mock/gomock"
	reflect "reflect"
)

// MockSampleRepository is a mock of SampleRepository interface
type MockSampleRepository struct {
	ctrl     *gomock.Controller
	recorder *MockSampleRepositoryMockRecorder
}

// MockSampleRepositoryMockRecorder is the mock recorder for MockSampleRepository
type MockSampleRepositoryMockRecorder struct {
	mock *MockSampleRepository
}

// NewMockSampleRepository creates a new mock instance
func NewMockSampleRepository(ctrl *gomock.Controller) *MockSampleRepository {
	mock := &MockSampleRepository{ctrl: ctrl}
	mock.recorder = &MockSampleRepositoryMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockSampleRepository) EXPECT() *MockSampleRepositoryMockRecorder {
	return m.recorder
}

// Save mocks base method
func (m *MockSampleRepository) Save(ctx context.Context, sample example.Sample) error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Save", ctx, sample)
	ret0, _ := ret[0].(error)
	return ret0
}

// Save indicates an expected call of Save
func (mr *MockSampleRepositoryMockRecorder) Save(ctx, sample interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockSampleRepository)(nil).Save), ctx, sample)
}

// Where mocks base method
func (m *MockSampleRepository) Where() example.SampleRepositorySearchSpec {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Where")
	ret0, _ := ret[0].(example.SampleRepositorySearchSpec)
	return ret0
}

// Where indicates an expected call of Where
func (mr *MockSampleRepositoryMockRecorder) Where() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Where", reflect.TypeOf((*MockSampleRepository)(nil).Where))
}

// MockSampleRepositorySearchSpec is a mock of SampleRepositorySearchSpec interface
type MockSampleRepositorySearchSpec struct {
	ctrl     *gomock.Controller
	recorder *MockSampleRepositorySearchSpecMockRecorder
}

// MockSampleRepositorySearchSpecMockRecorder is the mock recorder for MockSampleRepositorySearchSpec
type MockSampleRepositorySearchSpecMockRecorder struct {
	mock *MockSampleRepositorySearchSpec
}

// NewMockSampleRepositorySearchSpec creates a new mock instance
func NewMockSampleRepositorySearchSpec(ctrl *gomock.Controller) *MockSampleRepositorySearchSpec {
	mock := &MockSampleRepositorySearchSpec{ctrl: ctrl}
	mock.recorder = &MockSampleRepositorySearchSpecMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockSampleRepositorySearchSpec) EXPECT() *MockSampleRepositorySearchSpecMockRecorder {
	return m.recorder
}

// IDIn mocks base method
func (m *MockSampleRepositorySearchSpec) IDIn(id ...int) example.SampleRepositorySearchSpec {
	m.ctrl.T.Helper()
	varargs := []interface{}{}
	for _, a := range id {
		varargs = append(varargs, a)
	}
	ret := m.ctrl.Call(m, "IDIn", varargs...)
	ret0, _ := ret[0].(example.SampleRepositorySearchSpec)
	return ret0
}

// IDIn indicates an expected call of IDIn
func (mr *MockSampleRepositorySearchSpecMockRecorder) IDIn(id ...interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDIn", reflect.TypeOf((*MockSampleRepositorySearchSpec)(nil).IDIn), id...)
}

// NameLike mocks base method
func (m *MockSampleRepositorySearchSpec) NameLike(name string) example.SampleRepositorySearchSpec {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "NameLike", name)
	ret0, _ := ret[0].(example.SampleRepositorySearchSpec)
	return ret0
}

// NameLike indicates an expected call of NameLike
func (mr *MockSampleRepositorySearchSpecMockRecorder) NameLike(name interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NameLike", reflect.TypeOf((*MockSampleRepositorySearchSpec)(nil).NameLike), name)
}

// Find mocks base method
func (m *MockSampleRepositorySearchSpec) Find(ctx context.Context) (example.SampleList, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Find", ctx)
	ret0, _ := ret[0].(example.SampleList)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Find indicates an expected call of Find
func (mr *MockSampleRepositorySearchSpecMockRecorder) Find(ctx interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockSampleRepositorySearchSpec)(nil).Find), ctx)
}

// Get mocks base method
func (m *MockSampleRepositorySearchSpec) Get(ctx context.Context) (*example.Sample, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Get", ctx)
	ret0, _ := ret[0].(*example.Sample)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Get indicates an expected call of Get
func (mr *MockSampleRepositorySearchSpecMockRecorder) Get(ctx interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSampleRepositorySearchSpec)(nil).Get), ctx)
}

使用感

func TestMock2(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	filterMock := NewMockSampleRepositorySearchSpec(ctrl)
	filterMock.EXPECT().IDIn(gomock.Any()).Return(filterMock).AnyTimes()
	filterMock.EXPECT().NameLike(gomock.Any()).Return(filterMock).AnyTimes()
	
	repoMock := NewMockSampleRepository(ctrl)
	repoMock.EXPECT().Where().Return(filterMock)

	t.Run(`subtest2`, func(st *testing.T) {
        // モックを定義
		filterMock.EXPECT().Get(gomock.Any()).Return(&Sample{
			ID:   100,
			UUID: "xxx",
		} , nil)
		
		sample , err := repoMock.Where().IDIn(100).Get(context.Background())
		assert.NoError(st , err)
		assert.Equal(st  , 100 , sample.ID  ) 
	})

}

今回のケースでmockしたいのは、究極的には Find(ctx context.Context) Get(ctx context.Context) だけなので、メソッドチェーンの途中経過はどうでもいいのである。
そのあたり、 gomock.Any() というのが用意されている gomockはなかなか素敵かもしれない。

Expect()...XXX()...Return(好きな返り値)

という形でダミーで返す値を設定するが、引数がinterfaceなため、適切に仕様を満たさない返り値を設定してしまうと、実行時までエラーがわからない という難点はある。

今回はgomockのほうに好感が持てた。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?