Lento con forza

大学生気分のIT系エンジニアが色々書いてく何か。ブログ名決めました。

GolangはテストのためにInterfaceで公開しない

最近は趣味でGolangとTypeScriptを書いています。Golangのドキュメントを見ていると気になる記述を見つけたのでブログに書き残します。

ここに書いてあることです。 github.com

あるモジュールをテストしたい時に、モックのためにインターフェースで公開する場合があります。 ここではwikiの例をお借りして、Thingerという型がThingというメソッドを持っている場合について記述します。

// DO NOT DO IT!!!
package producer

type Thinger interface { Thing() bool }

type defaultThinger struct{ … }
func (t defaultThinger) Thing() bool { … }

func NewThinger() Thinger { return defaultThinger{ … } }

しかし、DO NOT DO IT!!! と書いてある通り、GolangではモックのためにInterfaceで公開するのではなく、実装をそのまま公開することが推奨されています。

package producer // producer.go

type Thinger struct{ … }
func (t Thinger) Thing() bool { … }

func NewThinger() Thinger { return Thinger{ … } }

このようにするとシンプルになります。では、利用側ではどのようにThingerを扱うのでしょうか。

package consumer  // consumer.go

type Thinger interface { Thing() bool }

func Foo(t Thinger) string { … }

producerを利用する側でThingerインターフェースを定義します。インターフェースは producer.Thinger のうち、利用するメソッドと同じものを定義します。こうすることで、producer.Thingerは後から定義したThingerインターフェースを満たすことになります。そうすることで、以下のようにThingerを生成することができます。

Fooメソッドを利用時は、以下のようにThingerを生成して扱うことができます。

// something.go
Foo(NewThinger())

Fooメソッドをテストしたい時は

package consumer // consumer_test.go
type fakeThinger struct { 
    thing bool
}
func (t fakeThinger) Thing() bool {
    return thing
}

func TestFooSuccess(t *testhing.T) {
    actual := Foo(fakeThinger{true})
    expected := "failure"
    if actual != expected {
        t.Errorf("got: %v\nwant: %v", actual, expected)
    }
}

func TestFooFailure(t *testing.T) {
    actual := Foo(fakeThinger{false})
    expected := "failure"
    if actual != expected {
        t.Errorf("got: %v\nwant: %v", actual, expected)
    }
}

このように、Thingerインターフェースのモックを生成し、自由にユニットテストを行うことができます。

このように、GolangのInterfaceでは振る舞いのみを定義するためこのようなことができます。

これは、すでに定義されているライブラリをモックしてテストしたい場合にも役立ちます。

例えばFirebaseのVerifyIDTokenを使った認証メソッドをモックしたい場合は、以下のようなInterfaceを定義します。

type firebaseClient interface {
    VerifyIDToken(context.Context, string) (*auth.Token, error)
}

func Auth(client firebaseClient) bool {
  // use VerifyIDToken method and return bool
}

Authを利用する時は

client := // Firebaseのクライアントを生成する
Auth(client)

のように記述し、Authをテストしたい時は

type fakeAuthClient struct {
}

func (f *fakeAuthClient) VerifyIDToken(ctx context.Context, token string) (*auth.Token, error) {
    return &auth.Token{
        UID:    "MockedUID",
    }, nil
}

func TestAuth(t *testing.T) {
    actual, _ := Auth(fakeAuthClient{})
    expected := "MockedUID"
    if actual.UID != expected {
        t.Errorf("got: %v\nwant: %v", actual.UID, expected)
    }
}

のように記述します。

こうすることでInterfaceで公開されていない既存のライブラリをテストすることもできるようになります。 このようにテストを行うことができるため、GolangではテストのためにInterfaceで公開することは推奨されていないのですね。

ついInterfaceで実装を公開してしまいそうになったのですが、このWikiをみてstructのまま公開することにしました。 とてもシンプルでやりたいことを実現できる良いドキュメントだったのでブログに残しておきます。