Slide 1

Slide 1 text

gomock完全に理解した
 @sanposhiho (Kensei Nakada)


Slide 2

Slide 2 text

お前 is 誰
 Kensei Nakada / さんぽし
 @sanpo_shiho
 
 
 - バックエンドエンジニア
 - 最近はお仕事でGoを触ることが多い
 - なんとgomockを完全に理解している
 


Slide 3

Slide 3 text

このトークの目指すところ
 gomockを完全に理解する


Slide 4

Slide 4 text

このトークの対象となる人
 
 gomockを完全に理解していない全人類


Slide 5

Slide 5 text

アジェンダ
 ● モックの扱い方を完全に理解する
 ○ 基本的な使用方法
 ○ cweill/gotestsにおけるgomockの使用
 ● gomockの中の仕組みを完全に理解する
 ○ どのように`期待される実行`などを管理しているか
 ● mockgenの扱い方を完全に理解する
 ○ mockgenの2つのモードとその違い
 ○ go generateを用いたmockgenの管理


Slide 6

Slide 6 text

golang/mock
 - go公式が出している
 - インターフェース定義からモックの生成を行うことができる (mockgen)
 - 生成したモックを扱うパッケージ群
 
 (以降のスライドではgomockと記載)


Slide 7

Slide 7 text


 モックの扱い方を完全に理解する


Slide 8

Slide 8 text

mockgenでモックを生成
 
 ↓こんな感じのコマンドを実行するとモックが生成される
 
 
 これ以降はUserインターフェースを元に作成したモックを
 例に説明します
 mockgen -source=user.go -destination=./mock type User interface { Update(user *entity.User) error }


Slide 9

Slide 9 text

mockgenでモックを生成
 
 
// Code generated by MockGen. DO NOT EDIT. // Source: user.go // Package mock_repo is a generated GoMock package. package mock_repo import ( entity "github.com/camphor-/relaym-server/domain/entity" gomock "github.com/golang/mock/gomock" reflect "reflect" ) // MockUser is a mock of User interface type MockUser struct { ctrl *gomock.Controller recorder *MockUserMockRecorder } // MockUserMockRecorder is the mock recorder for MockUser type MockUserMockRecorder struct { mock *MockUser } // NewMockUser creates a new mock instance func NewMockUser(ctrl *gomock.Controller) *MockUser { mock := &MockUser{ctrl: ctrl} mock.recorder = &MockUserMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use func (m *MockUser) EXPECT() *MockUserMockRecorder { return m.recorder } // Update mocks base method func (m *MockUser) Update(user *entity.User) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Update", user) ret0, _ := ret[0].(error) return ret0 } // Update indicates an expected call of Update ↓こんな感じのモックが生成される 


Slide 10

Slide 10 text

生成されるモックをみてみる
 
 モックのファイルには構造体が二つ定義されている
 
 // MockUser is a mock of User interface type MockUser struct { ctrl *gomock.Controller recorder *MockUserMockRecorder } // MockUserMockRecorder is the mock recorder for MockUser type MockUserMockRecorder struct { mock *MockUser }

Slide 11

Slide 11 text

生成されるモックをみてみる
 MockUserはUserインターフェースを満たす構造体
 
 // Update mocks base method func (m *MockUser) Update(user *entity.User) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Update", user) ret0, _ := ret[0].(error) return ret0 }

Slide 12

Slide 12 text

生成されるモックをみてみる
 MockUserMockRecorderはMockの呼び出しなどを管理する
 // Update indicates an expected call of Update func (mr *MockUserMockRecorder) Update(user interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUser)(nil).Update), user) }

Slide 13

Slide 13 text

モックの基本的な使用法
 var userEntity = &entity.User{ID: 12345} func TestMyThing(t *testing.T) {    // モックの呼び出しを管理するControllerを生成 mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockUser := mock_repo.NewMockUser(mockCtrl)    // テスト中に呼ばれるべき関数と帰り値を指定 mockUser.EXPECT().Update(userEntity).Return(nil) // do test... }

Slide 14

Slide 14 text

モックの基本的な使用法
 - Update関数が期待する引数で呼ばれるかをチェック
 - 今回は&entity.User{ID: 12345}の引数を期待
 - 呼ばれたとしても期待していない引数で呼ばれた場合もテストは失敗する
 
 - 正しく呼ばれていた場合、指定した返り値を返す
 - 今回は.Return(nil)と指定していたのでnilを返す


Slide 15

Slide 15 text

cweill/gotestsにおけるgomockの使用
 
 cweill/gotests
 テーブルドリブンテストの雛形を自動生成してくれるリポジトリ
 例えば↓このSearchTracks関数のテストを生成すると…
 type TrackUseCase struct { trackCli spotify.TrackClient } func (t *TrackUseCase) SearckTracks(ctx context.Context, q string) ([]*entity.Track, error) { return t.trackCli.Search(ctx, q) }


Slide 16

Slide 16 text

func TestTrackUseCase_SearckTracks(t1 *testing.T) { type fields struct { trackCli spotify.TrackClient } type args struct { ctx context.Context q string } tests := []struct { name string fields fields args args want []*entity.Track wantErr bool }{ // TODO: Add test cases. } for _, tt := range tests { t1.Run(tt.name, func(t1 *testing.T) { t := &TrackUseCase{ trackCli: tt.fields.trackCli, } got, err := t.SearckTracks(tt.args.ctx, tt.args.q) if (err != nil) != tt.wantErr { t1.Errorf("SearckTracks() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t1.Errorf("SearckTracks() got = %v, want %v", got, tt.want) } }) }

Slide 17

Slide 17 text

cweill/gotestsにおけるgomockの使用
 難点
 - テストケースによって「期待する呼び出し」「返したい値」が異な る
 - テストケースごとにEXPECT()....を使い分ける必要
 - そもそも呼び出したいかどうかすら異なる場合も
 


Slide 18

Slide 18 text

cweill/gotestsにおけるgomockの使用
 テストケースごとにモックの生成の関数を定義する
 
 tests := []struct { name string q string prepareMockFn func(m *mock_spotify.MockTrackClient) want []*entity.Track wantErr bool }{ { name: "success", q: "query1", prepareMockFn: func(m *mock_spotify.MockTrackClient) { m.EXPECT().Search(gomock.Any(), "query1").Return([]*entity.Track{{ID: "id1"}}, nil) }, want: []*entity.Track{{ID: "id1"}}, wantErr: false, }, }


Slide 19

Slide 19 text

cweill/gotestsにおけるgomockの使用
 for _, tt := range tests { t1.Run(tt.name, func(t1 *testing.T) { // controller(後述)の生成 ctrl := gomock.NewController(t1) defer ctrl.Finish() mock := mock_spotify.NewMockTrackClient(ctrl) // テストケースで定義した期待する呼び出しの設定 tt.prepareMockFn(mock) // モックの使用 t := &TrackUseCase{ trackCli: mock, } got, err := t.SearckTracks(context.Background(), tt.q)

Slide 20

Slide 20 text


 gomockの仕組みを完全に理解する


Slide 21

Slide 21 text

gomockのやっていること
 - 関数が期待する引数で呼ばれるかをチェック
 - 関数が呼ばれなかった場合、テストは失敗
 - 期待していない引数で呼ばれた場合、テストは失敗する
 
 - 正しく呼ばれていた場合、指定した返り値を返す
 


Slide 22

Slide 22 text

生成されたコードから仕組みを詳しく見てみよう
 
 登場人物
 - 生成されたモックのファイルの中にいたMock本体とrecorder
 // MockUser is a mock of User interface type MockUser struct { ctrl *gomock.Controller recorder *MockUserMockRecorder } // MockUserMockRecorder is the mock recorder for MockUser type MockUserMockRecorder struct { mock *MockUser }

Slide 23

Slide 23 text

生成されたコードから仕組みを詳しく見てみよう
 
 
登場人物
 - モックを使用するときに初めに作成していたcontroller
 
 for _, tt := range tests { t1.Run(tt.name, func(t1 *testing.T) { // ↓↓これ↓↓ ctrl := gomock.NewController(t1) defer ctrl.Finish()

Slide 24

Slide 24 text

生成されたコードから仕組みを詳しく見てみよう
 
 
 
 - これ↓は具体的に何を行なっているのか
 - ここで登録した呼び出しのチェックなどはどのように行なってい るか
 mockUser.EXPECT().Update(userEntity).Return(nil)

Slide 25

Slide 25 text

生成されたコードから仕組みを詳しく見てみよう
 - EXPECT()はMockUserのメソッドとして定義されている
 - recorderをそのまま返している
 
 // EXPECT returns an object that allows the caller to indicate expected use func (m *MockUser) EXPECT() *MockUserMockRecorder { return m.recorder }


Slide 26

Slide 26 text

生成されたコードから仕組みを詳しく見てみよう
 
 controllerに呼び出しの情報を記録している
 // Update indicates an expected call of Update func (mr *MockUserMockRecorder) Update(user interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() // controllerに対して`呼び出しの期待`を記録 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUser)(nil).Update), user) }

Slide 27

Slide 27 text

生成されたコードから仕組みを詳しく見てみよう
 
 実際の呼び出し時にctrl.Callを使用して呼び出されたことを controllerに伝えている
 同時にReturn()で指定した返り値をcontrollerから受け取って返却
 // Update mocks base method func (m *MockUser) Update(user *entity.User) error { m.ctrl.T.Helper()  // controllerに呼び出されたことを伝える ret := m.ctrl.Call(m, "Update", user) ret0, _ := ret[0].(error) return ret0 }

Slide 28

Slide 28 text

生成されたコードから仕組みを詳しく見てみよう
 
 (controller).Finish()
 → controllerに記録された期待されていた呼び出しが全て呼び出 されたかを確認
 for _, tt := range tests { t1.Run(tt.name, func(t1 *testing.T) { ctrl := gomock.NewController(t1) // ↓↓これ↓↓ defer ctrl.Finish()

Slide 29

Slide 29 text

余談
 ちなみに
 ctrl.Finish()はgomock v1.5.0+ (かつ Go v1.14+)では実行する必要 がなくなっています
 Go v1.14で追加されたt.Cleanupという関数で自動でctrl.Finish()を 実行してくれるようになったためです


Slide 30

Slide 30 text


 mockgenを完全に理解する


Slide 31

Slide 31 text

mockgenコマンドでモックの生成を行うことが出来る
 
 mockgenとは
 mockgen -source=user.go -destination=./mock

Slide 32

Slide 32 text

mockgenのモード
 mockgenコマンドには二つのモードが存在する
 Source モード
 
 
 Reflect モード
 
 
 mockgen -source=user.go [other options] mockgen database/sql/driver Conn,Driver [other options]

Slide 33

Slide 33 text

mockgenのモードの違い
 Source モード
 - 指定したファイル内の全てのインターフェースが対象
 - unexportedなインターフェースも対象
 - type aliasも正しく動作する
 
 Reflect モード
 - 指定したインターフェースのみが対象
 - unexportedなインターフェースは対象にできない
 - type aliasが正しく動作しない(らしい)
 


Slide 34

Slide 34 text

mockgenのモードどちらを使うべきか
 - 基本的にはReflect モードが不要なインターフェースのモックまで 生成されないので便利
 
 - unexportedなインターフェースのモックを生成したい場合などは Source モードを使用する


Slide 35

Slide 35 text

余談
 ちなみに… 「なんで二つもモードあるん?」というissue(#406)が立っていてメ ンテナが「ごめん、将来的にCLI上での明確な境界線はなくそうと 思ってるんやで〜」っていうコメントを残していたので将来的には 統合されたりするのかも?


Slide 36

Slide 36 text

モックの管理
 mockgenはgo generateコマンドとともに管理されることが多い
 
 
 各フォルダにこのようにモックを生成するmockgenコマンドを記載 しておくことでgo generateコマンドで全てのモックを更新できる
 //go:generate mockgen -source=user.go -destination=./mock

Slide 37

Slide 37 text

go generate + mockgenの課題点
 - go generateは並列実行しないようにデザインされている 
 - mockgenの実行は並列実行されて欲しい…
 - この管理法だとモックが最新のインターフェースを元に生成さ れているかわからない
 - 本来はCIで常に最新のインターフェースを元にモックが生成されていることを 保証したい
 - モックの定義がファイルに散らばる
 - 開発者によるモックの定義の仕方の差異が出やすい


Slide 38

Slide 38 text

gomockhandlerによるモック管理
 - 並列にモックを生成できる
 - モックが最新のインターフェースを元に生成されているかを確 認できる
 - モックの管理が一つのファイルのみを通して行われる
 
 


Slide 39

Slide 39 text

終わりに
 このトークを完全に理解したみなさんはgomockを完全に理解した といっても過言ではありません
 
 良いgomockライフをお過ごしください