$30 off During Our Annual Pro Sale. View Details »

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

sanposhiho
March 27, 2021
960

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

sanposhiho

March 27, 2021
Tweet

More Decks by sanposhiho

Transcript

  1. gomock完全に理解した

    @sanposhiho (Kensei Nakada)


    View Slide

  2. お前 is 誰

    Kensei Nakada / さんぽし

    @sanpo_shiho



    - バックエンドエンジニア

    - 最近はお仕事でGoを触ることが多い

    - なんとgomockを完全に理解している


    View Slide

  3. このトークの目指すところ

    gomockを完全に理解する


    View Slide

  4. このトークの対象となる人


    gomockを完全に理解していない全人類


    View Slide

  5. アジェンダ

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

    ○ 基本的な使用方法

    ○ cweill/gotestsにおけるgomockの使用

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

    ○ どのように`期待される実行`などを管理しているか

    ● mockgenの扱い方を完全に理解する

    ○ mockgenの2つのモードとその違い

    ○ go generateを用いたmockgenの管理


    View Slide

  6. golang/mock

    - go公式が出している

    - インターフェース定義からモックの生成を行うことができる
    (mockgen)

    - 生成したモックを扱うパッケージ群


    (以降のスライドではgomockと記載)


    View Slide


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


    View Slide

  8. mockgenでモックを生成


    ↓こんな感じのコマンドを実行するとモックが生成される



    これ以降はUserインターフェースを元に作成したモックを

    例に説明します

    mockgen -source=user.go -destination=./mock
    type User interface {
    Update(user *entity.User) error
    }


    View Slide

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


    View Slide

  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
    }

    View Slide

  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
    }

    View Slide

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

    View Slide

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

    View Slide

  14. モックの基本的な使用法

    - Update関数が期待する引数で呼ばれるかをチェック

    - 今回は&entity.User{ID: 12345}の引数を期待

    - 呼ばれたとしても期待していない引数で呼ばれた場合もテストは失敗する


    - 正しく呼ばれていた場合、指定した返り値を返す

    - 今回は.Return(nil)と指定していたのでnilを返す


    View Slide

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


    View Slide

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

    View Slide

  17. cweill/gotestsにおけるgomockの使用

    難点

    - テストケースによって「期待する呼び出し」「返したい値」が異な
    る

    - テストケースごとにEXPECT()....を使い分ける必要

    - そもそも呼び出したいかどうかすら異なる場合も


    View Slide

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


    View Slide

  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)

    View Slide


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


    View Slide

  21. gomockのやっていること

    - 関数が期待する引数で呼ばれるかをチェック

    - 関数が呼ばれなかった場合、テストは失敗

    - 期待していない引数で呼ばれた場合、テストは失敗する


    - 正しく呼ばれていた場合、指定した返り値を返す


    View Slide

  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
    }

    View Slide

  23. 生成されたコードから仕組みを詳しく見てみよう


    
登場人物

    - モックを使用するときに初めに作成していたcontroller


    for _, tt := range tests {
    t1.Run(tt.name, func(t1 *testing.T) {
    // ↓↓これ↓↓
    ctrl := gomock.NewController(t1)
    defer ctrl.Finish()

    View Slide

  24. 生成されたコードから仕組みを詳しく見てみよう




    - これ↓は具体的に何を行なっているのか

    - ここで登録した呼び出しのチェックなどはどのように行なってい
    るか

    mockUser.EXPECT().Update(userEntity).Return(nil)

    View Slide

  25. 生成されたコードから仕組みを詳しく見てみよう

    - EXPECT()はMockUserのメソッドとして定義されている

    - recorderをそのまま返している


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


    View Slide

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

    View Slide

  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
    }

    View Slide

  28. 生成されたコードから仕組みを詳しく見てみよう


    (controller).Finish()

    → controllerに記録された期待されていた呼び出しが全て呼び出
    されたかを確認

    for _, tt := range tests {
    t1.Run(tt.name, func(t1 *testing.T) {
    ctrl := gomock.NewController(t1)
    // ↓↓これ↓↓
    defer ctrl.Finish()

    View Slide

  29. 余談

    ちなみに

    ctrl.Finish()はgomock v1.5.0+ (かつ Go v1.14+)では実行する必要
    がなくなっています

    Go v1.14で追加されたt.Cleanupという関数で自動でctrl.Finish()を
    実行してくれるようになったためです


    View Slide


  30. mockgenを完全に理解する


    View Slide

  31. mockgenコマンドでモックの生成を行うことが出来る


    mockgenとは

    mockgen -source=user.go -destination=./mock

    View Slide

  32. mockgenのモード

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

    Source モード



    Reflect モード



    mockgen -source=user.go [other options]
    mockgen database/sql/driver Conn,Driver [other
    options]

    View Slide

  33. mockgenのモードの違い

    Source モード

    - 指定したファイル内の全てのインターフェースが対象

    - unexportedなインターフェースも対象

    - type aliasも正しく動作する


    Reflect モード

    - 指定したインターフェースのみが対象

    - unexportedなインターフェースは対象にできない

    - type aliasが正しく動作しない(らしい)


    View Slide

  34. mockgenのモードどちらを使うべきか

    - 基本的にはReflect モードが不要なインターフェースのモックまで
    生成されないので便利


    - unexportedなインターフェースのモックを生成したい場合などは
    Source モードを使用する


    View Slide

  35. 余談

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


    View Slide

  36. モックの管理

    mockgenはgo generateコマンドとともに管理されることが多い



    各フォルダにこのようにモックを生成するmockgenコマンドを記載
    しておくことでgo generateコマンドで全てのモックを更新できる

    //go:generate mockgen -source=user.go -destination=./mock

    View Slide

  37. go generate + mockgenの課題点

    - go generateは並列実行しないようにデザインされている 

    - mockgenの実行は並列実行されて欲しい…

    - この管理法だとモックが最新のインターフェースを元に生成さ
    れているかわからない

    - 本来はCIで常に最新のインターフェースを元にモックが生成されていることを
    保証したい

    - モックの定義がファイルに散らばる

    - 開発者によるモックの定義の仕方の差異が出やすい


    View Slide

  38. gomockhandlerによるモック管理

    - 並列にモックを生成できる

    - モックが最新のインターフェースを元に生成されているかを確
    認できる

    - モックの管理が一つのファイルのみを通して行われる



    View Slide

  39. 終わりに

    このトークを完全に理解したみなさんはgomockを完全に理解した
    といっても過言ではありません


    良いgomockライフをお過ごしください


    View Slide