🧪

【Go】uber-go/mockを使って、モックを活用してテストを行なってみる

2024/07/21に公開

goでテスト時のmockをどのように行うか分からなかったので、実際に試してみました。

golang/mockを使おうとしたところ、

readmeに以下の文が。
https://github.com/golang/mock

Update, June 2023: This repo and tool are no longer maintained. Please see go.uber.org/mock for a maintained fork instead.

こちらのリポジトリはもうメンテナンスがされていないようなので、uber-go/mockを使うことにしました。

Uberでgomockを多用していたため、自分達でフォークしメンテナンスをすることにしたそうです。

リスペクトと感謝です。
https://github.com/uber-go/mock

テスト対象のコード

postをフェッチして、そのpost数を返すサービス。
FetchPostを使用して実際にFetchを行います。DIの観点からInterfaceを指定しています。
今後でテストでこのインターフェース部分をモックします。

ライブラリを使わずにモックをする場合は、このinterfaceをimplementした構造体を作り、
CountPostServiceConstructerに渡してあげることでモックが可能となります。

CountPostService.go
package sample

type CountPostService struct {
	FetchPost FetchPostInterface
}

func CountPostServiceConstructer(FetchPost FetchPostInterface) CountPostService {
	return CountPostService{FetchPost}
}

func (fd *CountPostService) Execute() int {
	posts := fd.FetchPost.Fetch()
	return len(posts)
}
FetchPostInterface.go
package sample

type FetchPostInterface interface {
	Fetch() []Post
}

実際に使われている実装
テスト時に毎回外部APIは叩きたくありません。

FetchPost.go
package sample

import (
	"encoding/json"
	"io"
	"net/http"
)

type Post struct {
	UserID int    `json:"userId"`
	ID     int    `json:"id"`
	Title  string `json:"title"`
	Body   string `json:"body"`
}

type FetchPost struct{}

func FetchPostConstructor() FetchPost {
	return FetchPost{}
}

func (fp FetchPost) Fetch() []Post {
	resp, _ := http.Get("https://jsonplaceholder.typicode.com/posts")
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)

	var posts []Post;
	json.Unmarshal(body, &posts)

	return posts
}

gomockコマンドを実行

上記準備を終えて以下コマンドを実行しました。
sourceでインターフェースファイルを指定、destinationで自動生成ファイルの位置とファイル名の指定、packageでpackage名を指定しました。

mockgen -source=sample/FetchPostInterface.go -destination=sample/mock.go -package sample

生成されたファイル

gomockコマンドにより、以下ファイルが生成されました。
編集はしません。

mock.go
// Code generated by MockGen. DO NOT EDIT.
// Source: sample/FetchPostInterface.go
//
// Generated by this command:
//
//	mockgen -source=sample/FetchPostInterface.go -destination=sample/mock.go -package sample
//

// Package sample is a generated GoMock package.
package sample

import (
	reflect "reflect"

	gomock "go.uber.org/mock/gomock"
)

// MockFetchPostInterface is a mock of FetchPostInterface interface.
type MockFetchPostInterface struct {
	ctrl     *gomock.Controller
	recorder *MockFetchPostInterfaceMockRecorder
}

// MockFetchPostInterfaceMockRecorder is the mock recorder for MockFetchPostInterface.
type MockFetchPostInterfaceMockRecorder struct {
	mock *MockFetchPostInterface
}

// NewMockFetchPostInterface creates a new mock instance.
func NewMockFetchPostInterface(ctrl *gomock.Controller) *MockFetchPostInterface {
	mock := &MockFetchPostInterface{ctrl: ctrl}
	mock.recorder = &MockFetchPostInterfaceMockRecorder{mock}
	return mock
}

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

// Fetch mocks base method.
func (m *MockFetchPostInterface) Fetch() []Post {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Fetch")
	ret0, _ := ret[0].([]Post)
	return ret0
}

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

mockして、テストを行う

FetchPostInterfaceのFetchが、空のPostスライスを返すようにモックして、
テストすることができました。

CountPostService_test.go
package sample

import (
	"testing"

	"github.com/stretchr/testify/assert"
	gomock "go.uber.org/mock/gomock"
)

func TestCountPostService_Execute(t *testing.T) {
	ctrl := gomock.NewController(t)

	m := NewMockFetchPostInterface(ctrl)

	var posts []Post
	m.EXPECT().Fetch().Return(posts)

	countPostService := CountPostServiceConstructer(m)
	count := countPostService.Execute()
	
	assert.Equal(t, 0, count)
}

生成されたmock.goと共に軽く読む

略
ctrl := gomock.NewController(t)
m := NewMockFetchPostInterface(ctrl)

上記コードにより、mにはmock.goに定義されている構造体が代入されます。

type MockFetchPostInterface struct {
	ctrl     *gomock.Controller
	recorder *MockFetchPostInterfaceMockRecorder
}

またMockFetchPostInterfaceについてmock.goで

// Fetch mocks base method.
func (m *MockFetchPostInterface) Fetch() []Post {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Fetch")
	ret0, _ := ret[0].([]Post)
	return ret0
}

が定義されているため、
CountPostService.goで定義されているCountPostServiceConstructerの引数に指定可能になります。

また実際にFetchの戻り値を設定している部分について

m.EXPECT().Fetch().Return(posts)

MockFetchPostInterface構造体に定義されているEXPECTを実行し、
MockFetchPostInterfaceMockRecorder構造を返し、
MockFetchPostInterfaceMockRecorder構造に定義されているFetch(Fetchがmock.goに2つあるので注意)を実行し、
*gomock.Callが返されます。

Call構造のドキュメント
https://pkg.go.dev/go.uber.org/mock@v0.4.0/gomock#Call

ここで定義されているReturnを使って、Fetchの戻り値をmockしました。
他にも色々なfuncが用意されいるので、今後試していきたいです。

Discussion