ユニットテストが書きやすい設計〜自家製モックを添えて〜

## はじめに

Goでは`interface`を使うことで機能と実装を分けることができます。
関数やメソッドやがインターフェースにのみ依存するのであれば、テストの時にだけ実装を入れ替えることができ、テストしやすいコードになります。

ここでは、`interface`を使って、テストしやすいコード書き、ライブラリなどを使わずにモックを作成し、ユニットテストを書いてみようと思います。

## 作るもの

2つのユーザIDを受け取り、ユーザの名前が同じかどうか調べるというサービスを作ることにします。

このサービスは以下のインターフェースで表されます。

```go
type UserNameService interface {
	IsSameName(id1, id2 UserID) (bool, error)
}
```

2つのユーザIDを受け取って名前が等しければ`true`を返すというものです。
指定されたIDに対応するユーザが存在しない場合には`error`が返ってきます。


## 実装

### 準備

まずは、`User`を定義します。

```go:user.go
package sample

type UserID uint

type User struct {
	ID   UserID
	Name string
}
```

`UserNameService`はこの`User`オブジェクトの`Name`が等しいかを調べるのですが、`IsSameName`が引数で受け取るのは`UserID`なので、`UserID`から`User`を解決しなくてはなりません。
`UserNameService`の役割はユーザの名前に関する操作を行うことなので、`User`の解決を行うことは単一責任の原則に反します。
そこで`UserRepository`を定義して、`User`の解決を任せることにします。

`UserRepository`の定義は以下のようになります。

```go
type UserRepository interface {
	FindByID(UserID) (*User, error)
}
```

これで、`UserNameService`が実装できるようになりました。

### UserNameService

`UserRepository`を使った`UserNameService`の実装は以下のようになります。

```go:user_name_service.go
package sample

type UserNameService interface {
	IsSameName(id1, id2 UserID) (bool, error)
}

// UserRepositoryImplは UserRepository に依存している
type UserNameServiceImpl struct {
	userRepository UserRepository
}

func (impl *UserNameServiceImpl) IsSameName(id1, id2 UserID) (bool, error) {
	u1, err := impl.userRepository.FindByID(id1)
	if err != nil {
		return false, err
	}
	u2, err := impl.userRepository.FindByID(id2)
	if err != nil {
		return false, err
	}
	return (u1.Name == u2.Name), nil
}

func NewUserNameService() UserNameService {
	// 依存に実装を注入し、UserNameServiceを構築する
	return &UserNameServiceImpl{
		NewUserRepository(), // UserRepositoryを注入
	}
}
```

`UserNameServiceImpl`が`UserNameService`を実装しています。
内部に`UserRepository`を持っていることから`UserRepositoryImpl`は`UserRepository`に依存しているといえます。

`IsSameName`は`UserRepository`を使って`User`を検索し、名前が等しいかを調べています。

`UserNameService`の実装はこれで終わりですが、このサービス(厳密には`Impl`)はユニットテストのコードが書きやすい状態だといえるでしょう。
その理由として、

- 内部状態がない(`UserRepository`に`User`の管理を任せている)ため、メソッドが呼ばれたときには引数と`UserRepository`のみ考えればよい
- `UserRepository`はインターフェースなのでテスト時だけ実装を変更できる

ということがあります。
ユニットテストは、その名の通り1つの振る舞いに対するテストなので、他のユニットの影響を受ける状態では書くことが困難になります。(依存先の影響を受けることになる、つまり、依存先のユニットのテストも暗黙的に含んでしまうため。)
テスト時だけ実装を変更できるということは、依存先の動作を自在に制御することができるということです。
これによって、**依存先が想定内の動作をしている場合には自身も正しく動作することを保証するテスト**を書くことができます。

`NewUserNameService()`は`UserNameService`への依存に対して実装を注入する関数です。
内部で使用している`NewUserRepository()`は同様に`UserRepository`への依存に対して実装を注入する関数で、次節で定義します。
テスト時以外で実装を切り替えることはほとんどないと思うので、このようなデフォルトの依存性注入メソッドを定義し他から呼び出されるようにしても問題ないでしょう。


### UserRepository

`UserRepository`も実装します。
通常ならばKVSやRDBを使って`User`を検索することになると思いますが、今回は`map[UserID]*User`を使うことにします。

```go:user_repository.go
package sample

import (
	"errors"
)

type UserRepository interface {
	FindByID(UserID) (*User, error)
}

var NoSuchUserError = errors.New("no such user")

// UserRepositoryImplは map[UserID]*User に依存している
type UserRepositoryImpl struct {
	m map[UserID]*User
}

func (impl *UserRepositoryImpl) FindByID(id UserID) (*User, error) {
	user, ok := impl.m[id]
	if !ok {
		return nil, NoSuchUserError
	}
	return user, nil
}

func NewUserRepository() UserRepository {
	// 依存に実装を注入し、UserRepositoryを構築する
	m := make(map[UserID]*User)
	m[1] = &User{1, "tarou"}
	m[2] = &User{2, "tarou"}
	m[3] = &User{3, "hanako"}
	return &UserRepositoryImpl{m}
}
```

`UserRepository`は`map`という実装に依存していますが、ユニットテストが書きやすい状態だといえるでしょう。
なぜなら、`map`の実装をテスト時に入れ替えることはできませんが、`map`というデータ構造自体が内部状態を制御しやすいため、テスト時に任意の状態を作り出せるからです。
もちろん`map`のような振る舞いをするインターフェースを作ってそれに依存させたほうが柔軟な設計になりますが、今回の場合そこまでする必要はないでしょう。

ちなみに、今回ユニットテストを書きやすいのは`map`を使っているためであって、`database/sql`を使い`struct`である`DB`に依存するなどしているとユニットテストを書くのは大変だと思います。
この場合は`DB`を包むようなインターフェースを作ることも選択肢に入ります。

`NewUserRepository()`では`map`を注入し`UserRepository`を構築しています。
ここでは3つの`User`をハードコーディングしていますが、設定ファイルを通してRDBに接続しデータを読み込むなどしてもよいでしょう。

## ユニットテスト

### UserRepository

まずはシンプルにテストが書ける`UserRepository`から始めます。

```go:user_repository_test.go
package sample

import (
	"testing"
)

func TestUserRepository(t *testing.T) {
	m := make(map[UserID]*User)
	m[1] = &User{1, "tarou"}
	repository := &UserRepositoryImpl{
		m,
	}

	if u, _ := repository.FindByID(1); u == nil {
		t.Error("FindByID must be successful when the map including a specified key")
	}

	if _, err := repository.FindByID(2); err != NoSuchUserError {
		t.Error("FindByID must be fail with NoSuchUserError when no users have such id")
	}

	delete(m, 1)
	if _, err := repository.FindByID(1); err != NoSuchUserError {
		t.Error("FindByID must be fail with NoSuchUserError when a specified user was deleted")
	}
}
```

テストの初めに`map`を作成してIDが1の`User`を追加し、`UserRepository`に注入しています。
そして、

- `map`に含まれるユーザIDを指定した場合には`User`を返す
- `map`に含まれないユーザIDを指定した場合には`NoSuchUserError`を返す
- `map`からユーザが削除された場合には`NoSuchUserError`を返す

という3つの項目をテストしています。
`m[1] = &User{1, "tarou"}`や`delete(m, 1)`のように、メソッドを呼び出したときの`map`の状態を自在に制御できる(`m[1]`のような呼び出しの結果を制御できる)ため、`FindByID`のロジックのみをテストできています。

### モックの作成

`UserRepository`は`map`という容易に制御できるデータに依存していたため、テストをシンプルに書くことができました。
しかし、`UserNameService`の場合、依存する`UserRepository`の内部状態を自在に制御することはできません(`UserNameService`からは`UserRepository`のインターフェースしか見えないため)。
一方で、`UserRepository`は`interface`であるため、モックを作成すればテストにおいて都合のいい振る舞いをさせることが可能になります。

モックの作成には[gomock](https://github.com/golang/mock)のようなライブラリを使うこともできますが、自作してもそれほど手間がかからなかったり、モック動作がよくわからない場合の調査が楽だったりすると思うのでライブラリは使わないでおこうと思います。

作成した`UserRepository`のモックは以下のようになります。

```go:user_repository_mock.go
package sample

type UserRepositoryMock struct {
	FindByIDMock func(UserID) (*User, error)
}

func (m *UserRepositoryMock) FindByID(id UserID) (*User, error) {
	return m.FindByIDMock(id)
}
```

モックの作成は、実装するインターフェースのメソッドと同じ型の関数を持つ`struct`を作成し、インタフェースの実装ではその関数を呼び出すだけです。

### UserNameService

作成したモックを使って書いた`UserNameService`のユニットテストは以下のようになります。

```go:user_name_service_test.go
package sample

import (
	"testing"
)

func TestUserNameService(t *testing.T) {
	userRepositoryMock := &UserRepositoryMock{}
	service := &UserNameServiceImpl{
		userRepositoryMock,
	}

	count := 0
	userRepositoryMock.FindByIDMock = func(id UserID) (*User, error) {
		count++
		return &User{id, "tarou"}, nil
	}
	if b, _ := service.IsSameName(1, 2); b != true {
		t.Error("IsSameName must be true when users have a same name")
	}
	if count != 2 {
		t.Error("IsSameName must call UserRepository.FindByID just twice")
	}

	userRepositoryMock.FindByIDMock = func(id UserID) (*User, error) {
		if id == 1 {
			return &User{1, "tarou"}, nil
		} else if id == 2 {
			return &User{2, "hanako"}, nil
		} else {
			return nil, NoSuchUserError
		}
	}
	if b, err := service.IsSameName(1, 2); b != false || err != nil {
		t.Error("IsSameName must be false when users have diffferent names")
	}

	userRepositoryMock.FindByIDMock = func(id UserID) (*User, error) {
		if id == 1 {
			return &User{1, "tarou"}, nil
		} else {
			return nil, NoSuchUserError
		}
	}
	if _, err := service.IsSameName(1, 2); err != NoSuchUserError {
		t.Error("IsSameName must be fail with NoSuchUserError when some user not found")
	}
}
```

テストの初めに`UserRepository`のモックを作成し、`UserNameService`に注入しています。
ここでは以下の(3+1)つのテストを行っています。

- 名前が同じユーザの場合には`true`を返す
- (一度の`IsSameName`で`UserRepository.FindByID`が2回呼ばれる)
- ユーザの名前が異なる場合には`false`を返し、かつ、その場合の`error`は`nil`である
- ユーザが見つからない場合には`NoSuchUserError`を返す

各テストの前に`FindByIDMock`に期待する動作を設定しているため、`IsSameName`のロジックのみをテストできています。

モックを作成するライブラリでは、メソッドが呼ばれた回数を調べられるようになっていることがありますが、今回の1つめのテストのように外部にカウンタを設置することで実現できます。
毎回`func(...){...}`を書かなければならないので冗長な部分もありますが、`X.Expect().Method("Y").Return(Z)`のようなライブラリ依存の構文を覚えなくてよいという利点もあります。

今回`UserNameService`に依存するメソッドなどは作りませんでしたが、もし依存するものがあれば`UserRepositoryMock`と同じようにモックを作成してテストを書くことができます。

## まとめ

インターフェースと実装を分けることによってユニットテストが書きやすいコードを書けたと思います。
今回書いたコードの設計をパターン化すると以下のようになります。

1. 作成したい機能をインタフェースで表現し、実装は`Impl`に書く
2. 依存先に実装を注入するために`New+インターフェース名`という名前で引数をとらない関数を作る
3. テスト時に振る舞いを変えられるようにモックを作成する

このようなパターンで設計すればユニットテストの書き方で悩むことが減るのではないでしょうか。