moqでinterfaceのMockを作る

この記事は Go3 Advent Calendar 2017の18日目の記事です。

moqでMockを作る

Goでアプリケーションを書いているとinterfaceにMockを投げ込みたくなる瞬間があると思います。 たとえば、以下のようなコードのテストを書きたい場合ですね。 Userを取りに行く処理が外部に依存しているためにClient interfaceを切っていて、それをApplication構造体が持っています。

type User struct {
    ID        int
    FirstName string
    LastName  string
}

type Client interface {
    GetAllUser() ([]*User, error)
}

type Application struct {
    Client Client
}
func (a *Application) FilterLastName(name string) ([]*User, error) { users, _  := a.Client.GetAllUser()

    var result []*User
    for _, u := range users {
        if u.LastName == name {
            result = append(result, u)
        }
    }

    return result, nil
}

Goはダックタイピングなので、以下のようにClient interfaceを満たす構造体を自分で定義するのもいいですが、 interfaceが持つメソッドが多いと自分で定義するのも面倒です。

type Mock struct {
    FilterLastNameFunc func(string) ([]*User, error)
}

func (m *Mock) FilterLastName(name string) ([]*User, error) {
    return m.FilterLastNameFunc(name)
}

となれば「楽をしよう!」ということで、この記事ではMockを自動生成してくれる moqを紹介します。

他にも幾つかMockを自動生成してくれるツールはありますが、他のツールに比べてmoqが優れているところは、

自動生成されたMockが余計なパッケージをimportしていない

という点にあります。

とりあえず以下のコマンドでインストールします。

$ go get github.com/matryer/moq

インストールできたらMockを生成してみます。

$ moq -out client_mock.go . Client

以下が生成したclient_mock.goの内容です。

// Code generated by moq; DO NOT EDIT
// github.com/matryer/moq

package main

import (
    "sync"
)

var (
    lockClientMockGetAllUser sync.RWMutex
)

// ClientMock is a mock implementation of Client.
//
//     func TestSomethingThatUsesClient(t *testing.T) {
//
//         // make and configure a mocked Client
//         mockedClient := &ClientMock{
//             GetAllUserFunc: func() ([]*User, error) {
//                    panic("TODO: mock out the GetAllUser method")
//             },
//         }
//
//         // TODO: use mockedClient in code that requires Client
//         //       and then make assertions.
//
//     }
type ClientMock struct {
    // GetAllUserFunc mocks the GetAllUser method.
    GetAllUserFunc func() ([]*User, error)

    // calls tracks calls to the methods.
    calls struct {
        // GetAllUser holds details about calls to the GetAllUser method.
        GetAllUser []struct {
        }
    }
}

// GetAllUser calls GetAllUserFunc.
func (mock *ClientMock) GetAllUser() ([]*User, error) {
    if mock.GetAllUserFunc == nil {
        panic("moq: ClientMock.GetAllUserFunc is nil but Client.GetAllUser was just called")
    }
    callInfo := struct {
    }{}
    lockClientMockGetAllUser.Lock()
    mock.calls.GetAllUser = append(mock.calls.GetAllUser, callInfo)
    lockClientMockGetAllUser.Unlock()
    return mock.GetAllUserFunc()
}

// GetAllUserCalls gets all the calls that were made to GetAllUser.
// Check the length with:
//     len(mockedClient.GetAllUserCalls())
func (mock *ClientMock) GetAllUserCalls() []struct {
} {
    var calls []struct {
    }
    lockClientMockGetAllUser.RLock()
    calls = mock.calls.GetAllUser
    lockClientMockGetAllUser.RUnlock()
    return calls
}

Lockの処理があるため、syncパッケージをimportしていますが、それ以外にimportをしているものはありません。 基本的にやっていることはMock構造体を定義していたときと同じです。

以下のように使うことができます。

mock := &ClientMock{
    GetAllUserFunc: func() ([]*User, error) {
        users := []*User{
            &User{ID: 1, FirstName: "Taro", LastName: "Suzuki"},
            &User{ID: 2, FirstName: "Taro", LastName: "Tanaka"},
            &User{ID: 3, FirstName: "Jiro", LastName: "Sato"},
            &User{ID: 4, FirstName: "Saburo", LastName: "Tanaka"},
        }
        return users, nil
    },
}
app := &Application{
    Client: mock,
}
users, err := app.FilterLastName("Tanaka")
if err != nil {
    panic(err)
}
for _, u := range users {
    fmt.Printf("%+v\n", u)
}

また、~~Callsというメソッドで、当該のメソッドが何回呼ばれたかもチェックすることができます。

fmt.Printf("call count: %d\n", len(mock.GetAllUserCalls()))

~~Func変数に値が入っていないときには、そのことを示した上でpanicを発生させてくれるので、 メソッドの数が多いinterfaceのMockを生成したときにも必要な部分だけを実装させるのもそこまで難しくありません。

moqに合わないユースケース

上記例のようなClient interfaceとそれを持つ構造体が同じパッケージ内部に存在する場合にはとても重宝しますが、 外部のパッケージのMockを生成しようとするとちょっと使いづらいです。

たとえば以下のようにfooパッケージとbarパッケージでそれぞれ提供されている Client interfaceを作りたい場合はつらみが出てきます。

type Application struct {
    FooClient foo.Client
    BarClient bar.Client
}

moqで指定するinterfaceのパスは、Goのimportパスではなく、普通のディレクトリパスのため、 生成されたファイルのimportに相対パスが入り込んでしまいます。

import (
    "./vendor/github.com/nametake/foo"
    "sync"
)

また、生成されたMockの構造体は、{interface名}Mockという名前になるので、上記のように別のパッケージで 同じ名前のinterfaceを利用している場合は、どちらもClientMockという名前になるので、ビルドでコケてしまいます。

まとめ

ユースケースに合わない部分もありますが、Mockの生成のようなやり方が定形になっている部分は どんどん自動化をして楽をしていきましょう!