概要
GO言語のテストコードモジュール GoMockを紹介してます。
GoMockはテストを実行するときにDBや外部APIを疎通しなくてもプログラミングが正しく動いているかどうかテストコードで確認することができます。
目次
GoMockの導入手順
GoMockを使ったコードはGithub上で公開しています。HomeAPIというプロジェクトの中でGoMockテストコードを記載しています。以下のGitHubボタンをクリックするとGitHubページに遷移します
GoMock インストール
go mockを利用するにはモジュールをインストールする必要があります。
以下のコマンドを入力してください。
go install github.com/golang/mock/mockgen
Go 1.16以上 go instalコマンドが1.16以上のため
GoMock コードを導入
go generateをコード内に記入
Go言語ではコードを自動生成するコマンド go generate が存在します。
クリーンアーキテクチャの設計であればrepository インターフェース層の以下にgo:generateを追記します。
全体的なコード
package repository import ( "homeapi/domain" "homeapi/interfaces/repository" "github.com/jinzhu/gorm" ) //go:generate mockgen -source=./temperature_repository.go -package=repositorymock -destination=./mock/temperature_repository.go func NewTemperatureRepository(db *gorm.DB) repository.TemperatureRepository { return repository.TemperatureRepository{ Database: db, } } // TemperatureRepository Temperature Repository type TemperatureRepository interface { List() ([]domain.Temperature, error) Insert(*domain.Temperature) error }
プロジェクト直下でコマンド
go generateのコメントを入れたら以下のコマンドを実行してください。
自動生成されたコード
インターフェースに合わせたリクエストとレスポンスの値に対してモックが作成されます。
// Code generated by MockGen. DO NOT EDIT. // Source: ./temperature_repository.go // Package repositorymock is a generated GoMock package. package repositorymock import ( domain "homeapi/domain" reflect "reflect" gomock "github.com/golang/mock/gomock" ) // MockTemperatureRepository is a mock of TemperatureRepository interface. type MockTemperatureRepository struct { ctrl *gomock.Controller recorder *MockTemperatureRepositoryMockRecorder } // MockTemperatureRepositoryMockRecorder is the mock recorder for MockTemperatureRepository. type MockTemperatureRepositoryMockRecorder struct { mock *MockTemperatureRepository } // NewMockTemperatureRepository creates a new mock instance. func NewMockTemperatureRepository(ctrl *gomock.Controller) *MockTemperatureRepository { mock := &MockTemperatureRepository{ctrl: ctrl} mock.recorder = &MockTemperatureRepositoryMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockTemperatureRepository) EXPECT() *MockTemperatureRepositoryMockRecorder { return m.recorder } // Insert mocks base method. func (m *MockTemperatureRepository) Insert(arg0 *domain.Temperature) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Insert", arg0) ret0, _ := ret[0].(error) return ret0 } // Insert indicates an expected call of Insert. func (mr *MockTemperatureRepositoryMockRecorder) Insert(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockTemperatureRepository)(nil).Insert), arg0) } // List mocks base method. func (m *MockTemperatureRepository) List() ([]domain.Temperature, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List") ret0, _ := ret[0].([]domain.Temperature) ret1, _ := ret[1].(error) return ret0, ret1 } // List indicates an expected call of List. func (mr *MockTemperatureRepositoryMockRecorder) List() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockTemperatureRepository)(nil).List)) }
このコードを修正するのはNGです。同じコマンドを打つとまた元に戻ってしまいます。
テストコード実装
テストコードを書いている場所はこちらをクリック
取得系のコード(SELECT)
type serverMocks struct { temperatureRepository *repositorymock.MockTemperatureRepository } func newMocks(ctrl *gomock.Controller) (*TemperatureUsecase, *serverMocks) { mocks := &serverMocks{ temperatureRepository: repositorymock.NewMockTemperatureRepository(ctrl), } temperatureUsecase := &TemperatureUsecase{ TemperatureRepository: mocks.temperatureRepository, } return temperatureUsecase, mocks } func TestList(t *testing.T) { t.Run("success", func(t *testing.T) { nowTime := time.Now() ctrl := gomock.NewController(t) defer ctrl.Finish() temperatureUsecase, mocks := newMocks(ctrl) temperature := []domain.Temperature{ { ID: 12, Temp: "22", Humi: "61", CreatedAt: nowTime, }, { ID: 13, Temp: "25", Humi: "63", CreatedAt: nowTime, }, } mocks.temperatureRepository.EXPECT().List().Return(temperature, nil) got, err := temperatureUsecase.List() require.NoError(t, err) if err != nil { t.Errorf("error message : %v", err) } want := []ports.TemperatureOutputPort{ { ID: 12, Temp: "22", Humi: "61", }, { ID: 13, Temp: "25", Humi: "63", }, } assert.Equal(t, &want, got) }) }
挿入系のコード(INSERT)
func TestInsert(t *testing.T) { t.Run("success", func(t *testing.T) { nowTime, err := util.JapaneseNowTime() if err != nil { t.Error(err) } // nowTime := time.Now() ctrl := gomock.NewController(t) defer ctrl.Finish() temperatureUsecase, mocks := newMocks(ctrl) temperature := &domain.Temperature{ Temp: "20", Humi: "55", CreatedAt: nowTime, } dofunc := func(temperature *domain.Temperature) *domain.Temperature { temperature.ID = 71 return temperature } mocks.temperatureRepository.EXPECT().Insert(temperature).Do(dofunc).Return(nil) request := &ports.TemperatureInputPort{ Temp: "20", Humi: "55", } got, err := temperatureUsecase.Create(request) require.NoError(t, err) want := ports.TemperatureOutputPort{ ID: 71, Temp: "20", Humi: "55", } assert.Equal(t, want, got) }) }
ハマりポイント
mocks.temperatureRepository.EXPECT().List().Return(temperature, nil)
Insertに対するコード
mocks.temperatureRepository.EXPECT().Insert(temperature).Do(dofunc).Return(nil)
EXPECT()の部分はなかなかテストが通りずらいことを経験された方が多いです。具体的になにを入れれがいいのか解説していきます。データは正しく挿入しないと動かないということとエラーメッセージがわかりずらいところです。
Listに対するインターフェース
List() ([]domain.Temperature, error)
Insertに対するインターフェース
Insert(*domain.Temperature) error
基本的に同じ型のデータを入れてあげればいいのですがInsertにあるdofuncはID等のデータベースで自動で割り振られるものを定義しておくことで予想するテスト結果を返すことができます。
まとめ
テストではDBぐらいはテストDB立ち上げてテストした方がより明確にテストできるのでいいですが、外部APIを利用する時はまさにMockを使った方が安心して利用することができます。 フリーランスエンジニアは正社員よりも労働時間が短いです。その分スキルアップに時間を割くこともできるし趣味に時間を使うこともできます。正社員よりも圧倒的にフリーランスエンジニアのほうが充実した人生を過ごせるというテーマです。 続きを見る
正社員からフリーランスのエンジニア転身でブラック企業とは無縁