Goで外部と通信するAPI Clientなどを使用しているのテストをMockをつかって書く方法を紹介します。

実装

今回は、下記のように User という構造体が fetchUserData という外部からユーザー情報を取得してくるメソッドを持っている例です。
前提として、ユーザー情報を取ってくるAPI Clientは get というメソッドで外部からデータを取得するものとします。

package sample

type User struct {
	Name string
	Age  int
}

type ApiClient interface {
	get(userId int) (name string, age int, err error)
}

func (u *User) fetchUserData(client ApiClient, userId int) error {
	var err error
	u.Name, u.Age, err = client.get(userId)
	if err != nil {
		return err
	}
	return nil
}

fetchUserData は引数に ApiClient という interface を満たすものをとるようになっています。 この場合、ユーザー情報を取ってくるAPI Clientは get というメソッドを持っている想定なので、 interface に get を書いておきます。
これで実際に fetchUserData を使用する際API Clientを注入すれば期待した通りの動きになりそうですね。

テストコード

さて、上記の実装に対するテストは下記のようになります。 ApiClient という interface を満たす ApiClientMock を作成します。 ApiClientMock が持つ get というメソッドは、実際に外部からデータを持ってくる get メソッドMockとして働きます。 このMockされたgetメソッドは ApiClientMock のfieldに代入された各情報を返すようになっています。

package sample

import (
	"errors"
	"testing"

	"github.com/google/go-cmp/cmp"
)

type apiClientMock struct {
	Name string
	Age  int
	err  error
}

func (a apiClientMock) get(userId int) (name string, age int, err error) {
	return a.Name, a.Age, a.err
}

func TestUser_fetchUserData(t *testing.T) {
	type args struct {
		client ApiClient
		userId int
	}
	tests := []struct {
		name     string
		u        *User
		args     argsT
		wantUser *User
		wantErr  bool
	}{
		{
			name: "正しくユーザー情報が取得できること",
			u:    &User{},
			args: args{
				client: apiClientMock{
					Name: "テスト 太郎",
					Age:  25,
					err:  nil,
				},
				userId: 1,
			},
			wantUser: &User{
				Name: "テスト 太郎",
				Age:  25,
			},
			wantErr: false,
		},
		{
			name: "取得できなかった場合エラーが返ること",
			u:    &User{},
			args: args{
				client: apiClientMock{
					err: errors.New("fail get"),
				},
				userId: 1,
			},
			wantUser: &User{},
			wantErr:  true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := tt.u.fetchUserData(tt.args.client, tt.args.userId); (err != nil) != tt.wantErr {
				t.Errorf("User.fetchUserData() error = %v, wantErr %v", err, tt.wantErr)
			}
			if diff := cmp.Diff(tt.u, tt.wantUser); diff != "" {
				t.Errorf("User value is mismatch (-get + want):\n%s", diff)
			}
		})
	}
}

終わりに

今回は、Goで外部と通信するAPI Clientなどを使用しているのテストをMockをつかって書く方法を紹介しました。 いままでinterfaceをあまり使ったことがなかったのですが、抽象化することで今回やったようなMockを注入できるようになったりできるんだな~と思いました。 自分自身、ほかの言語で外部との通信部をMockに置き換えてテストを書くなどやってきていたのですが、Goでは少してこずったので誰かの参考になれば幸いです。
また、もっとこういったいい方法があるよなど、アドバイスがあればTwitterのDMなどにぜひ送ってくれると喜びます。