Help us understand the problem. What is going on with this article?

Go Mockでインタフェースのモックを作ってテストする #golang

More than 3 years have passed since last update.

Go Mockとは?

https://github.com/golang を漁っていたら,Go Mockというものを見つけました。
github上での最初のコミットが2011年なので,かなり昔からあるようです。
名前からして何かのモックを作るライブラリだということは分かります。
READMEを見ると以下のように説明されています。

GoMock is a mocking framework for the Go programming language. It integrates well with Go's built-in testing package, but can be used in other contexts too.

どうやらtestingパッケージと一緒に使うようです。

インストール

READMEに書いてある通り,インストールしてみましょう。
どうやら,gomockというパッケージとmockgenというコマンドラインツールのインストールが必要なようです。

$ go get github.com/golang/mock/gomock
$ go get github.com/golang/mock/mockgen

インタフェースのモックを作る

mockgenを使うと与えたGoのソースからそのコード中にあるインタフェースのモックを作ってくれるようです。
以下のように,Sampleというインタフェースだけを定義したソースコードsample.gomockgenに与えてみましょう。

sample.go
package sample

type Sample interface {
    Method(s string) int
}

mockgenは以下のように実行します。

$ mockgen -source sample.go

どうでしょう?標準の出力にGoのソースが表示されてびっくりしたことでしょう。
今度は-destinationをつけてファイルに吐き出しましょう。

$ mkdir -p mock_sample
$ mockgen -source sample.go -destination mock_sample/mock_sample.go

以下のようなファイルがmock_sample以下に作られたはずです。

mock_sample/mock_sample.go
// Automatically generated by MockGen. DO NOT EDIT!
// Source: sample.go

package mock_sample

import (
    gomock "github.com/golang/mock/gomock"
)

// Mock of Sample interface
type MockSample struct {
    ctrl     *gomock.Controller
    recorder *_MockSampleRecorder
}

// Recorder for MockSample (not exported)
type _MockSampleRecorder struct {
    mock *MockSample
}

func NewMockSample(ctrl *gomock.Controller) *MockSample {
    mock := &MockSample{ctrl: ctrl}
    mock.recorder = &_MockSampleRecorder{mock}
    return mock
}

func (_m *MockSample) EXPECT() *_MockSampleRecorder {
    return _m.recorder
}

func (_m *MockSample) Method(s string) int {
    ret := _m.ctrl.Call(_m, "Method", s)
    ret0, _ := ret[0].(int)
    return ret0
}

func (_mr *_MockSampleRecorder) Method(arg0 interface{}) *gomock.Call {
    return _mr.mock.ctrl.RecordCall(_mr.mock, "Method", arg0)
}

生成されたMockSampleという構造体がSampleインタフェースを実装していることが分かります。
また,_MockSampleRecorderという構造体も作られています。
名前から察するに,何かを記録しておくものだということが予想できます。
まずは使ってみて,何がおきるのか試してみましょう。

モックを使ってテストを書く

まずは,以下のようなテストファイルsample_test.goを用意してください。
なお,このファイルはsample.goと同じディレクトリにおいてください。

sample_test.go
package sample

import (
    "testing"

    "github.com/golang/mock/gomock"

    mock "./mock_sample"
)

func TestSample1(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockSample := mock.NewMockSample(ctrl)
    mockSample.EXPECT().Method("hoge").Return(1)

    t.Log("result:", mockSample.Method("hoge"))
}

つぎに,テストを走らせてみます。-test.vでログが表示されるようにします。

$ go test -test.v
=== RUN   TestSample1
--- PASS: TestSample1 (0.00s)
        sample_test.go:18: result 1
PASS
ok

さて,このテストで何を行ったのか,ひとつずつ説明していきます。

最初に,gomcok.NewController(t)で,gomock.Controllerを作成しています。
defer ctrl.Finish()で後処理をしているのですが,これがなぜ必要なのかは後述します。

次に,Sampleインタフェースのモックを作っています。
そして,EXPECT().Method()を呼び出すことで,テスト中に作成したモックのMethodが呼び出されることを期待します。
EXPECTメソッドは,mock_sample.goで宣言されている,_MockSampleRecorderのポインタを返します。
*_MockSampleRecorderのメソッドを呼び出すと,*gomock.Controller.Finishによって後処理されるまでに,対応するモックのメソッドが呼び出されないとエラーになります。
この場合は,*_MockSampleRecorder.Methodメソッドを呼び出してから,*gomock.Controller.Finishを呼び出すまでに,*MockSample.Methodメソッドを呼びださなければいけません。
つまり,EXPECTを使えば,モックのメソッドが呼び出されたのかテストすることができます。

EXPECTを呼び出すと,*gomock.Callが返って来ます。
Go Mockのドキュメントを見ると,*gomock.Callはいくつか便利そうなメソッドを持っています。
ここでは,Returnメソッドを使っていますが,このメソッドを使えば,モックのMethodメソッドを呼び出した時の戻り値を設定できます。

最後にモックのMethodメソッドを呼び出して,Returnメソッドで設定した1が返って来ていることが分かります。

複雑なテストを書いてみる

先ほどのサンプルより,もう少し複雑なテストを書いてみましょう。

メソッドの呼び出し順序を指定する

メソッドの呼び出しの順番を指定するには,gomock.InOrder関数や*gomock.Call.Afterメソッドを使用します。
以下のように,呼び出す順番を指定し,この順番にメソッドが実行されない場合はテストが失敗します。

func TestSample2(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockSample := mock.NewMockSample(ctrl)

    // 呼び出される順番を指定
    gomock.InOrder(
        mockSample.EXPECT().Method("hoge").Return(1),
        mockSample.EXPECT().Method("fuga").Return(2),
    )
    /* // 上記と同じ
    mockSample.EXPECT().Method("hoge").Return(1).After(
        mockSample.EXPECT().Method("fuga").Return(2),
    )
    */

    t.Log("result", mockSample.Method("hoge"))
    t.Log("result", mockSample.Method("fuga"))
}

メソッドの呼び出し回数を指定する

メソッドの呼び出し回数を指定するには,*gomock.Call.Timesメソッドを使用します。
なお,呼び出し回数を指定しない場合は,デフォルトで1回ちょうどだけ呼び出さないとテストが失敗します。
そのため,何回も呼び出しても良い場合は,AnyTimesメソッドで複数回呼べることを指定します。

func TestSample3(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockSample := mock.NewMockSample(ctrl)

    // 呼び出される回数を指定
    mockSample.EXPECT().Method("hoge").Return(1).Times(2)

    // 何回でも良い場合は,AnyTimes
    // mockSample.EXPECT().Method("hoge").Return(1).AnyTimes()

    t.Log("result", mockSample.Method("hoge"))
    t.Log("result", mockSample.Method("hoge"))
}

既存パッケージのモックを作りたい

残念ながら,Go Mockでは既存パッケージのモックを作成したり,指定したソースコード中にないインタフェースのエイリアス型やインタフェースの埋込みを行った型のモックを作ることができません。
つまり,io.Writerのモックを作ったり,以下のような型を作ってモックを作ることができません。

// エイリアス型もダメ
type MyWriter1 io.Writer

// 埋込みもダメ
type MyWriter2 interface {
    io.Writer
}

しかし,幸いにもGoのインタフェースはダックタイピングを採用しています。
そのため,モックを作りたい既存パッケージのインタフェース(ここではio.Writerインタフェース)とメソッドリストが全く同じインタフェースをテスト対象の自分のパッケージ中に作ってやれば,モックを作ることができます。
そしてそのインタフェースは,既存パッケージのインタフェースとメソッドリストが全く同じであるため,作ったモックは,自前のインタフェースを実装するとともに,既存パッケージのインタフェースも実装していることになります。

sample.go
package sample

// io.Writerと同じ
type writer interface {
    Write([]byte) (int, error)
}
sample_test.go
package sample

import (
    "fmt"
    "testing"

    "github.com/golang/mock/gomock"

    mock "./mock_sample"
)

func TestSample(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    // sample.writerとio.Writerを共に実装
    w := mock.NewMockwriter(ctrl)

    gomock.InOrder(
        w.EXPECT().Write([]byte("hoge")).Return(4, nil),
        w.EXPECT().Write([]byte("fuga")).Return(4, nil),
    )

    // io.Writerとして渡す
    fmt.Fprintf(w, "hoge")
    fmt.Fprintf(w, "fuga")
}

この方法の問題点は,既存パッケージのメソッドリストに変更があった場合に,テストが失敗してしまうことです。
テスト対象のメソッドに変更があった場合に,テストが失敗するのはむしろ好都合のように思えますが,新しいメソッドが追加になった場合は別に失敗する必要はないので困ります。
しかし,Goのインタフェースは,一般的にはあまり大きなメソッドリストは持たず,io.Writerio.Readerのように特定のメソッドに特化したインタフェースを作ることが多いでしょう。
そのため,メソッドの追加や削除が発生することは稀なので,あまり心配することは無いでしょう。
そもそも,業務などでは,外部のパッケージのバージョンを固定して作業することが多いのではないでしょうか。

おわりに

この記事ではGo Mockについて調べ,簡単な使い方について説明しました。
今回使ったソースコードはgithubにあげておくので,ぜひ手元で動かしてみてください。

tenntenn
Go engineer / Gopher artist
mercari
フリマアプリ「メルカリ」を、グローバルで開発しています。
https://tech.mercari.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした