🤖

🤖

:gijutsu_burogu:

Goの自前テストモックでメソッド呼び出し回数を数える

はじめに

Go では、インターフェースを使い DI することで実装を置き換えることができます。 例えば、テストで外部 API を使う場合に実際のリクエストを飛ばさないようにテスト用に実装を置き換えたりします。 Go では特別なライブラリを使うことなく、標準的な機能を使うのみで実現することができます。

kotaroooo0-dev.hatenablog.com

テストでモックしたメソッドの呼び出し回数をカウントしたいことがあります。 例えば、重い処理をする際に結果を Redis にキャッシュしておき、キャッシュがあればそれを返すという処理のテストをする際に、キャッシュが使われているか判断するために、メソッドの呼び出し回数を見て判断する時があります。

どのようにメソッドの呼び出し回数を数えるのがいいでしょうか。 僕は、モック用の構造体に呼び出し回数のフィールドを付け加えることで解決しました。 しかし、これだとメソッド名が増えた時にフィールド数が多くなってしまうという弱点はあります。 みなさん、どのように解決していますか。

実装

例えば、以下の実装があったとします。 Service は Repository に依存しており、Repository のメソッドを呼び出します。 Service の FindUserById で何度 Repository の FindUser が呼ばれたかをテストしたいとします。

// Service
package mock_callcount

type UserService interface {
    FindUser(string) (User, error)
}

type UserServiceImpl struct {
    UserRepository *UserRepository
}

func (s *UserServiceImpl) FindUserById(id string) (User, error) {
    u, err := s.UserRepository.FindUser(id)
    if err != nil {
        return User{}, err
    }
    return u, nil
}

type UserRepository interface {
    FindUser(string) (User, error)
}

type User struct {
    Id   string
    Name string
}
// Repository
package mock_callcount

import "github.com/go-redis/redis/v7"

type UserRepositoryImpl struct {
    Client *redis.Client
}

func (r *UserRepositoryImpl) FindUser(id string) (User, error) {
    // なんか
    return User{Id: id}, nil
}

テスト

シンプルな実装(メソッド呼び出し回数を数えられない)

以下は Service のテストで、Repository をモックしています。 Service で Repository が何度呼び出されたかをテストしようとしています。 しかし、メソッドの呼び出し回数を数えることはできません。

package mock_callcount

import (
    "testing"

    "github.com/go-redis/redis/v7"
)

type UserRepositoryMock struct {
    Client *redis.Client
}

func (r *UserRepositoryMock) FindUser(id string) (User, error) {
    // なんか
    return User{Id: id}, nil
}

func TestFindUserById(t *testing.T) {

    userRepositoryMock := UserRepositoryMock{Client: testClient}
    userService := UserService{
        UserRepository: &userRepositoryMock,
    }

    userService.FindUserById()
    userService.FindUserById()
    // ここでUserRepositoryのFindUserメソッドは何度呼ばれたかをテストしたい
    // ここではServiceのFindByIdとRepositoryのFindUserは1:1なので明らかに2回
}

フィールドを追加した実装(メソッド呼び出し回数を数えられる)

Repository モックのフィールドに呼び出し回数に関するフィールドを追加します。 また、該当メソッドが呼び出されるとそのフィールドをインクリメントします。 Repository モックのフィールドを参照することで呼び出し回数を取得できます。

package mock_callcount

import (
    "testing"

    "github.com/go-redis/redis/v7"
)

type UserRepositoryMock struct {
    Client            *redis.Client
    // フィールドを追加します
    FindUserCallCount int
}

func (h UserRepositoryMock) FindUser(id string) (User, error) {
    // メソッドが呼ばれたらカウントをインクリメントします
    h.FindUserCallCount++
    // なんか
    return User{Id: id}, nil
}

func TestFindUserById(t *testing.T) {

    userRepositoryMock := UserRepositoryMock{Client: testClient}
    userService := UserService{
        UserRepository: &userRepositoryMock,
    }

    userService.FindUserById()
    userService.FindUserById()
    // 呼び出し回数を取得できます
    callCount := userService.UserRepository.FindUserCallCount
}

テストライブラリでは

github.com

stretchr/testifyを調べてみました。 同様に、構造体のフィールドに呼び出し回数を保持し、メソッドが呼び出されるたびにインクリメントしていました。

// Call represents a method call and is used for setting expectations,
// as well as recording activity.
type Call struct {
    // 略
    // Amount of times this call has been called
    totalCalls int
    // 略
}

// MethodCalled tells the mock object that the given method has been called, and gets
// an array of arguments to return. Panics if the call is unexpected (i.e. not preceded
// by appropriate .On .Return() calls)
// If Call.WaitFor is set, blocks until the channel is closed or receives a message.
func (m *Mock) MethodCalled(methodName string, arguments ...interface{}) Arguments {
    // 略
    call.totalCalls++
    // 略
}

おわりに

以上のように、テストにおいてコンポーネントへのアクセスを記録するものをスパイと言うそうです。 「メソッドは呼び出されたかどうか」「メソッドは何回呼び出されたか」「メソッドに渡された実引数は何だったか」等を検証できます。

かつて、スパイライブラリのSinon.JSを使った記憶が蘇りました。

var object = {
  get test() {
    return this.property;
  },
  set test(value) {
    this.property = value * 2;
  },
};

var spy = sinon.spy(object, "test", ["get", "set"]);

object.test = 42;
assert(spy.set.calledOnce);

assert.equals(object.test, 84);
assert(spy.get.calledOnce);

このように簡単に呼び出し回数をテストすることができます。 Go でスパイしたいとき、どのようにしているか、おすすめのライブラリ等あればコメント欲しいです。