gomockを完全に理解する

14 min read読了の目安(約12600字

この記事は 3/27 に開催されたCAMPHOR- DAY内で発表した内容を元にした記事です。

アーカイブはこちら
(共有しているスライドの画面が黄色くなっていることは終わってから知りました🥺 )

https://youtu.be/QVIU3hsQ24Q

この記事では gomock や mockgen の基本的な使用方法から、 gomock の内部の動きまでを紹介します。この記事を読み終わったあなたは思わずgomock 完全に理解した と言っていることでしょう。

基本的にGoの文法を知っていればgomock自体を知らなくても理解できるような説明にしているつもりです。

golang/mock(gomock) とは

go 公式が出しているインターフェース定義からモックの生成を行うことができるライブラリです。

生成したモックを扱うパッケージも含まれます。

https://github.com/golang/mock

この先の説明では gomock と呼ぶことにします

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

モックを生成してみる

mockgenに関して詳しくは後述しますが、以下のようなコマンドでモックが生成されます。

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

これ以降は以下のインターフェースを元に生成されたモックを元に説明を行います。

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

以下のようなモックが生成されます(ここでは詳しく読む必要はないので読み飛ばしてください)

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

生成されたモックを見てみる

生成されたモックを覗いてみましょう。モックのファイルには構造体が 2 つ定義されています。

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

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
}

MockUserMockRecorderは Mock の呼び出しなどを管理する構造体です。こちらは基本的には gomock を扱う際には意識する必要がない構造体です。後の説明で詳しく役割を説明します。

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

モックの基本的な使用法

基本的には以下のように使用します。

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

必要なのは

  1. モックの呼び出しを管理する Controller の生成
  2. モックの生成
  3. テスト中に呼ばれるべき関数と返り値を指定
  4. テスト本体を書く

の 4 ステップです。

上記の例だと

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

が行われます。

cweill/gotests における gomock の使用

cweill/gotests とはテーブルドリブンテストの雛形を生成してくれるツールです。GoLand や VScode から使用している人もかなり多いと思います。

https://github.com/cweill/gotests

テーブルドリブンテストに関しては以下に詳しい記述があります。

https://github.com/golang/go/wiki/TableDrivenTests

実際に以下の関数のテストを生成してみると…

type TrackUseCase struct {
    trackCli spotify.TrackClient
}
func (t *TrackUseCase) SearckTracks(ctx context.Context, q string) ([]*entity.Track, error) {
    return t.trackCli.Search(ctx, q)
}

以下のようなテストが生成されます

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

TODO: Add test casesのコメントのところに入力値と期待される出力値を構造体に詰めて書いていくことになります。

ここでの難点はテストケースによって「期待する呼び出し」「返したい値」が異なるということです。
テストケースごとにEXPECT()...を使い分ける必要があったりそもそも呼び出したいかどうかすら異なる場合があります。

一般的な解決策として、構造体にモックの生成の関数も含めるという手法があります。

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

このようにprepareMockFnという関数を構造体に追加しています。これによってテストケースごとに期待する呼び出しを設定できます。

  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)

            // モックの使用(DI)
            t := &TrackUseCase{
                trackCli: mock,
            }
            got, err := t.SearckTracks(context.Background(), tt.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)
            }
        })
    }

実行するパートでは上記のように関数を通してテストケースごとにモックの設定を行います。

Return()以外の選択肢

返り値の指定にReturn()というものを紹介していましたが、このほかに幾つか指定の方法があります。

DoAndReturn()は呼び出された際に関数を実行し、その返り値を返すという特殊なもので、これを用いることで、単純に値を比較しづらい引数を比較することができるなどの良い点があります。
単純なReturn()で実装できなくなった場合はDoAndReturn()の使用を検討してみると解決する場合が多いです。

m.EXPECT().Search(gomock.Any(), "query1").DoAndReturn(
  func(ctx context, q string) (*entity.Track, error) {
    // hogehoge
    return track, err
  }
)

gomock の仕組みを理解する

このパートでは生成されたコードが何をしているのかを少し覗いていきます。

gomock のやっていること

前述のように gomock は以下のようなことを行なっています

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

    • 関数が呼ばれなかった場合、テストは失敗
    • 期待していない引数で呼ばれた場合、テストは失敗する
  • 正しく呼ばれていた場合、指定した返り値を返す

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

実際に生成されたコードを少し覗いていきましょう。

以下の登場人物が出てきます

  • 生成されたモックのファイルの中にいたモック本体
  • 生成されたモックのファイルの中にいた recorder
  • モックを使用するときに初めに作成していた controller

の 3 人です。

モック本体と recorder に関してはこれです。MockUserは Controller と MockUserMockRecorderMockUserMockRecorderMockUserをフィールドとして持っています。

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

モックを使用するときに初めに作成していた controller というのはgomock.NewController()で生成しているものです。

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

それでは実際に以下の「期待する呼び出し」の設定を行う 1 文で何が起こっているのかをみていきましょう

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

まずはEXPECT()に関してみていきます。これは生成されたコード内にMockUserのメソッドとして定義されています。
この一文、何がmockの呼び出しとして期待されていて呼ばれた時に何を返すのか、 gomock を知らなくても理解することができますよね。すごい。

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

みてわかるようにMockUser構造体の recorder フィールドをそのまま返しています。このフィールドにはMockUserMockRecorderが挿入されています。

すなわち、mockuser.EXPECT().Update(userEntity)MockUserMockRecorderのメソッドであるUpdate()を呼び出しています。これも生成されたコード内に定義されています。

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

この関数の中ではMockUserがもつ controller のメソッドであるRecordCallWithMethodTypeを呼び出しています。

この関数によって controller に対して「呼び出しの期待」に関する記録をおこなます。

そしてMockUserに実装されているUpdateメソッドを見てみましょう。これが実際にテスト中に呼び出されるメソッドです。

// Update mocks base method
func (m *MockUser) Update(user *entity.User) error {
    m.ctrl.T.Helper()
 // controllerに呼び出されたことを伝える & 返り値をcontrollerからもらう
    ret := m.ctrl.Call(m, "Update", user)
    ret0, _ := ret[0].(error)
    return ret0
}

この関数の中で重要なのはm.ctrl.Call(m, "Update", user)です。この一文では

  • controller に呼び出されたことを伝える
  • controller から返り値をもらう

ということを行なっています。
controller からもらった返り値を元に Update 関数から値を返却しています。

ここまででわかったように gomock は controller を通して「呼び出しの期待」「返り値」などを管理しています。

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

そしてこの controller の生成の後にdeferで実行していたctrl.Finish()で controller に登録された情報をもとに「呼び出されることが期待されている関数が呼び出されているかどうか」を確認しています。

余談として、このctrl.Finish()はこれまで、実行しないと「呼び出されることが期待されている関数が呼び出されているかどうか」の確認が行われず、正しく gomock の力を発揮しきれないのが現状でした。

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

https://github.com/golang/mock/issues/407

mockgen を理解する

前述のように以下のようにmockgenコマンドでモックの生成を行うことができます。

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

mockgen の二つのモード

mockgen には実は「Source モード」「Reflect モード」の二つのモードが存在します。それぞれ少し実行の仕方が異なっています。(先程の例は Source モードです)

Source モード

mockgen -source=user.go [other options]

Reflect モード

mockgen database/sql/driver Conn,Driver [other options]

mockgen のモードの違い

それぞれ基本的なことが行えるという点であまり違いは存在しません。しかし微妙な違いが存在します。

Source モード

  • 指定したファイル内の全てのインターフェースが対象
  • unexported なインターフェースも対象
  • type alias も正しく動作する

Reflect モード

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

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

個人的には基本的には Reflect モードが不要なインターフェースのモックまで生成されることがないので便利だと思っています。しかし、unexported なインターフェースのモックを生成したい場合などは Source モードを使用する他なさそうです。

また、余談ですが、このように基本的には二つのモードに大きな差異はありません。そこで以下のような issue が立っています。

https://github.com/golang/mock/issues/406

この issue 内でメンテナの方が以下のように発言しています。

Also, I would like to know these difference so that in the future we can blur the lines and the cli just works. I would love to deprecate the need for some of the flags too eventually. Thanks for the report.

将来的には明示的に使い分けるということはなくなるかもしれません。

go generate コマンドを使用した mockgen の管理

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

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

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

go generate + mockgen の課題点

go generate コマンドで mockgen の管理を行うことは以下の課題点があります。

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

gomockhandler によるモック管理

そこで以下の gomockhandler というライブラリを最近開発していました。

以下の特徴があります。

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

https://github.com/sanposhiho/gomockhandler

詳しくは以下の記事で紹介しています。

https://engineering.mercari.com/blog/entry/20210406-gomockhandler/

終わりに

gomock は生成されるコードを通して、テストコードから gomock を知らない人でもパッと見て何が起こるかをなんとなく理解するように設計されている素晴らしいライブラリです。中身やある程度の利用方法をしることで、その洗練とされているデザインに少し触れられたのではないでしょうか

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