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

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

sanposhiho
March 27, 2021
1.2k

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

sanposhiho

March 27, 2021
Tweet

Transcript

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


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

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

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

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

  4. 生成されるモックをみてみる
 
 モックのファイルには構造体が二つ定義されている
 
 // 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 }
  5. 生成されるモックをみてみる
 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 }
  6. 生成されるモックをみてみる
 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) }
  7. モックの基本的な使用法
 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... }
  8. 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) } }) }
  9. 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, }, }

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

    range tests { t1.Run(tt.name, func(t1 *testing.T) { // ↓↓これ↓↓ ctrl := gomock.NewController(t1) defer ctrl.Finish()
  13. 生成されたコードから仕組みを詳しく見てみよう
 
 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) }
  14. 生成されたコードから仕組みを詳しく見てみよう
 
 実際の呼び出し時に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 }
  15. mockgenのモード
 mockgenコマンドには二つのモードが存在する
 Source モード
 
 
 Reflect モード
 
 


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


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

  17. go generate + mockgenの課題点
 - go generateは並列実行しないようにデザインされている 
 - mockgenの実行は並列実行されて欲しい…
 -

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