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の生成のようなやり方が定形になっている部分は どんどん自動化をして楽をしていきましょう!

Golang.tokyo#11でLTしてきた

アドベントカレンダーとLT同じ時期だし同じネタでやったろw」って思ってたら完全にネタがかぶったので、アドベントカレンダーとは別記事で公開。

LTしてきました。

ベストスピーカー賞頂けました。ありがとうございます。

頂いたGolang.tokyo仕様モバイルバッテリー、大事に使います。

f:id:nametake-1009:20171217172456j:plain:w300

GocCon2017感想とか

行ったので書く(VimConfの記録は出遅れたのであとでこっそり)。

パネルディスカッション

VimConfに続きmattnさんを招待しての他セッション。

聞きながら取ったメモを貼り付けておきます。抜け漏れは許容していただけると。

Keynote

Digital Oceanのエンジニアであり、vim-goの作者であるFatih氏の発表。

Go+Microservices at Mercari

https://talks.godoc.org/github.com/tcnksm/talks/2017/11/gocon2017/gocon2017.slide#1

メモ : gRPCを触ってみる。

Story of our own Monitoring Agent in golang

Gocon2017:Goのロギング周りの考察

個人的に、Goのロギングは下みたいなLoggerのinterfaceを用意して 使いたいライブラリを薄くWrap、DIすればなんでも良いかなぁ、と思ってるんですがどうなんですかね?

type Logger interface {
    Fatalf(...interface{})
    Errorf(...interface{})
    Warnf(...interface{})
    Infof(...interface{})
    Debugf(...interface{})
}

How to achieve parallel compilation in Go 1.9

Async, Persistent, Fast, and Sable "Enought" Queue/Worker Using Go and PostgreSQL

Diff algorithm in Go

TODO スライドを追加する。

EBITEN

https://docs.google.com/presentation/d/e/2PACX-1vSSbSxPObBZcJHjvUpAt-HEJVLaux2FQBpJbvbxInJgmEhxSn-lVxTVxUMmUNQwtJtC8w6_HkhuW2hk/pub?start=false&loop=false&delayms=3000&slide=id.p

reviewdog and static analysis for Go

https://docs.google.com/presentation/d/1_BWQXamZvIhL3l9ziL9zb25yP9RjpgXoxkWX-48ECss/edit#slide=id.p

今後

込み入ったGoの部分については掘っていないのであんまり凝った話はできませんが、パッケージ構成とかについてはAdventCalendarとかに書いていこうかなと思います。

golang.tokyo #5 感想

4月27日に開催されたGolang.tokyo#5に行ってきた。 GCPUG Tokyoとの共同開催ということで30分枠はGAEでGo言語を使ったお話。

直前に人数を見たら珍しく定員割れをしてましたそれでも100人超えてるあたりGoの注目度の高さが伺える。

GO *GAE

株式会社カブクさんの方の発表(資料未公開)。 GAEの基本的な部分の説明と、実際に業務で使ったときのお話。 Go言語は業務で使っていますが、GCPは触れたことがない勢なので基本的なところを説明していただいてとてもありがたかった。

以下、気になったところ箇条書きで書く。

  • GAEにはStandardとFlexibleの2種類ある
    • Standard Environment(SE)
      • PaaS
      • スピンアップが速い
      • Goは1.6
    • Flexble Environment(FE)
      • Dockerのコンテナを立ち上げる
      • バイナリを扱える
  • 基本はSEを使う
  • バイナリを扱いたいならFEしかない
  • カブク内で使っているところは3D解析のAPI
  • SEとFEを両方合わせて使用している
  • SEだけだと他の3D解析APIのリクエストに60秒以上かかるためタイムアウトする
  • FEだとTaskQueueが使えない

社内でもGCPを使う話はちょこちょこ聞くけど60秒制限はよく引っかかりそうなのでちゃんと覚えておく。 あと、辛かったことで言っていた事と全く同じことを自分もGoを最初に触った時に思っていたのでわかりがあった。

GAE/GOの勘どころ

株式会社ソウゾウの@osamingoさんの発表。

以下、気になったところ箇条書きで書く。

  • CloudSQLの最大同時接続数が12(Master,Slave合計で)
  • Vendoringは出来るらしい(質疑応答より)
  • gb
  • direnv
  • Deployもよしなに
  • Monitoringも楽
  • GCPは運用が楽
  • GCPは運用が楽(重要)

GCPでの細かいハマりどころとかツール等々。 AWSの運用めんどくさくない?と最近気づき始めたのでGCPの運用が楽という話を聞いて触りたくなった。

Datastore/Go のデータ設計と struct の振る舞いについて

ここからLT枠。 @pospomeさんのDatastoreでの設計のお話。

設計の話は特に好きなのでfmfmと思いながら聞いていたが、@vvakameさんの

というツイートを見てなるほど自動生成系ツールもあるのかという知見を得た。

となると@__timakin__さんもツイートしていたように、サービスロジックで扱う構造体とDatastoreで扱う構造体を分離するのが、 ベターかなぁという感じがした。

Contextアンチパターン

神(Context)を殺した話。

面白かった(小並感)。

自分ではまだContext.Valueを使っていなかったが、使う時には気をつける。

あと、現在自分のチームではechoを使っているが、スライドの内容と同じように殆どRoutingにしか使ってないんだったら少し重い。 なので、最近は、個人でWebのものを書くときとかプロダクトから少し離れたツールを書くときは httprouterを使っている。

ASTライブコーディング

@tenntennさんのASTライブコーディング。

以下のコードから文字列のhoge変数だけを取り出すコードを10分で書いていた(すごい)。

package main

import (
    "fmt"
)

func main() {
    var hoge = "hoge"
    fmt.Println(hoge)

    func() {
        var hoge = 0
        fmt.Println(hoge)
    }()
}

触ろう触ろうと思ってastは全然触っておらず、いい機会でもあったので自分でも書いてみた。

https://gist.github.com/nametake/1fd82014bdfc434a32aba6590c4c66bb

(@tenntennさんがやっていたやり方とは違う気がするけど取り出せてるからセーフ)

astを触ったことがなくてもast.Printの内容を見ていれば何となくどうなっているのかわかって、思っていた以上に楽ちんに書けた。

fish-shellのpecoプラグインをforkした

最近fishに乗り換えた。

pluginはfishermanで管理することにしたが、そうなるとoh-my-fish/plugin-pecoの以下の点が気に食わなかった。

  • fish_user_key_bindingsに自分でbind設定を書く必要がある
  • pecoのlayoutがbottom-up

fishermanを使っているのであればfish_user_key_bindingsになるべく自分設定を書き込みたくなかったので、forkしてnametake/plugin-pecoを作った。

JAWS DAYS 2017行ってきた

OSCも捨てがたかったけど最近業務でAWSを触っていたのもあって、JAWS DYASに参加してきた。

完全に遅刻気味だけどとりあえず自分が見たセッションについてスライドと内容をまとめてみる。

続きを読む