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 というメソッドを持っている想定なので、 interfaceget を書いておきます。
これで実際に 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などにぜひ送ってくれると喜びます。