Upgrade to Pro — share decks privately, control downloads, hide ads and more …

gomock完全に理解した / I completely understand gomock.

F1e600f2c53b0456e0b6c34009d4127e?s=47 sanposhiho
March 27, 2021
420

gomock完全に理解した / I completely understand gomock.

F1e600f2c53b0456e0b6c34009d4127e?s=128

sanposhiho

March 27, 2021
Tweet

Transcript

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


  2. お前 is 誰
 Kensei Nakada / さんぽし
 @sanpo_shiho
 
 


    - バックエンドエンジニア
 - 最近はお仕事でGoを触ることが多い
 - なんとgomockを完全に理解している
 

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


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


  5. アジェンダ
 • モックの扱い方を完全に理解する
 ◦ 基本的な使用方法
 ◦ cweill/gotestsにおけるgomockの使用
 • gomockの中の仕組みを完全に理解する
 ◦

    どのように`期待される実行`などを管理しているか
 • mockgenの扱い方を完全に理解する
 ◦ mockgenの2つのモードとその違い
 ◦ go generateを用いたmockgenの管理

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


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


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

    type User interface { Update(user *entity.User) error }

  9. 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 ↓こんな感じのモックが生成される 

  10. 生成されるモックをみてみる
 
 モックのファイルには構造体が二つ定義されている
 
 // 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 }
  11. 生成されるモックをみてみる
 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 }
  12. 生成されるモックをみてみる
 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) }
  13. モックの基本的な使用法
 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... }
  14. モックの基本的な使用法
 - Update関数が期待する引数で呼ばれるかをチェック
 - 今回は&entity.User{ID: 12345}の引数を期待
 - 呼ばれたとしても期待していない引数で呼ばれた場合もテストは失敗する
 
 -

    正しく呼ばれていた場合、指定した返り値を返す
 - 今回は.Return(nil)と指定していたのでnilを返す

  15. 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) }

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


  18. 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, }, }

  19. 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)
  20. 
 gomockの仕組みを完全に理解する


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


  22. 生成されたコードから仕組みを詳しく見てみよう
 
 登場人物
 - 生成されたモックのファイルの中にいた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 }
  23. 生成されたコードから仕組みを詳しく見てみよう
 
 
登場人物
 - モックを使用するときに初めに作成していたcontroller
 
 for _, tt :=

    range tests { t1.Run(tt.name, func(t1 *testing.T) { // ↓↓これ↓↓ ctrl := gomock.NewController(t1) defer ctrl.Finish()
  24. 生成されたコードから仕組みを詳しく見てみよう
 
 
 
 - これ↓は具体的に何を行なっているのか
 - ここで登録した呼び出しのチェックなどはどのように行なってい るか
 mockUser.EXPECT().Update(userEntity).Return(nil)

  25. 生成されたコードから仕組みを詳しく見てみよう
 - EXPECT()はMockUserのメソッドとして定義されている
 - recorderをそのまま返している
 
 // EXPECT returns an

    object that allows the caller to indicate expected use func (m *MockUser) EXPECT() *MockUserMockRecorder { return m.recorder }

  26. 生成されたコードから仕組みを詳しく見てみよう
 
 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) }
  27. 生成されたコードから仕組みを詳しく見てみよう
 
 実際の呼び出し時に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 }
  28. 生成されたコードから仕組みを詳しく見てみよう
 
 (controller).Finish()
 → controllerに記録された期待されていた呼び出しが全て呼び出 されたかを確認
 for _, tt :=

    range tests { t1.Run(tt.name, func(t1 *testing.T) { ctrl := gomock.NewController(t1) // ↓↓これ↓↓ defer ctrl.Finish()
  29. 余談
 ちなみに
 ctrl.Finish()はgomock v1.5.0+ (かつ Go v1.14+)では実行する必要 がなくなっています
 Go v1.14で追加されたt.Cleanupという関数で自動でctrl.Finish()を

    実行してくれるようになったためです

  30. 
 mockgenを完全に理解する


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

  32. mockgenのモード
 mockgenコマンドには二つのモードが存在する
 Source モード
 
 
 Reflect モード
 
 


    mockgen -source=user.go [other options] mockgen database/sql/driver Conn,Driver [other options]
  33. mockgenのモードの違い
 Source モード
 - 指定したファイル内の全てのインターフェースが対象
 - unexportedなインターフェースも対象
 - type aliasも正しく動作する


    
 Reflect モード
 - 指定したインターフェースのみが対象
 - unexportedなインターフェースは対象にできない
 - type aliasが正しく動作しない(らしい)
 

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


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


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

    -source=user.go -destination=./mock
  37. go generate + mockgenの課題点
 - go generateは並列実行しないようにデザインされている 
 - mockgenの実行は並列実行されて欲しい…
 -

    この管理法だとモックが最新のインターフェースを元に生成さ れているかわからない
 - 本来はCIで常に最新のインターフェースを元にモックが生成されていることを 保証したい
 - モックの定義がファイルに散らばる
 - 開発者によるモックの定義の仕方の差異が出やすい

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


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