TL;DR
- テスト(=mock)のためだけに
interface
は切りたくない - 型エイリアスとビルドタグを組み合わせると
interface
がなくてもモックが作れる - この手法に必要なモックを自動生成するプログラムを作った
interface
は本当に必要なシーンで使うべき
Background
現在モックを使った単体テストは一般的です。
Javaでの例を挙げると、モックしたいコンポーネントについて予めinterface
を定義しておき、モックではそのインターフェースを実装するのが定石です。
しかしgolangのinterface
はJavaなどのそれとは若干性質が異なるため、テスト=モックのためだけにinterface
を書くのはオーバーワーク気味です。
そうテストのためだけにinterface
を書くのはまっぴらごめんなのです。
interface
をテストのために書くのが嫌すぎて、いろいろ頭をひねっていたら書かなくて良い方法を思いつきました。
その方法を端的に言うと、型エイリアスで外のパッケージにある型を自分の空間にインポートし、ビルドタグでテストの時だけその実装をモックに入れ替える、となります。
ではサンプルコードを示しましょう。
以下はサンプルプロジェクトのディレクトリ構成です。
典型的なTodoアプリのDDD風味を想定して、do/
にはドメインオブジェクトを、repository
には永続化を、service
にはビジネスロジックを置く形にしています。
github.com/koron/todo-noif-mock/
+-- do/
| +-- todo.go
+-- repository/
| +-- todo_repository.go
+-- service/
+-- repository.go
+-- repository_mock.go
+-- todo_service.go
+-- todo_service_test.go
まず do/todo.go
は Todo
オブジェクトを定義するだけですから極めて単純です。
DTO (Data Transfer Object)と言っても良いでしょう。
package do
type Todo struct {
ID int64
Task string
// ... そのほかTodoに要求されるフィールドを定義する
}
repository/todo_repository.go
は、とりあず Insert()
メソッドを持つだけとしておきましょう。
これは説明のための簡略化ですから本来は別名のメソッドであっても良いし、複数あっても良いです。
package repository
import "github.com/koron/todo-noif-mock/do"
type Repository struct {
}
func (r *Repository) Insert(todo *do.Todo) (*do.Todo, error) {
// 典型的なDB操作(insert)がココに入る
return todo, nil
}
この時に service/repository.go
として型エイリアスとビルドタグを用いた以下のような内容のファイルを作成します。
このファイルはビルドタグ mock
が指定されなければコンパイル対象となります。
// +build !mock
package service
import "github.com/koron/todo-noif-mock/repository"
type Repository = repository.Repository
一方でモックの実態は service/repository_mock.go
に以下のように定義します。
このファイルはビルドタグ mock
が指定されたときだけコンパイル対象となります。
// +build mock
package service
import "github.com/koron/todo-noif-mock/do"
type Repository struct {
// モックに必要なフィールドをここに置く
}
func (m *Repository) Insert(todo *do.Todo) (*do.Todo, error) {
// モックのコードをココに書く
return todo, nil
}
勘の良い方にはもう説明の必要はないでしょうが、
service/todo_service.go
と service/todo_service_test.go
のコードを続けて示しましょう。
package service
import "github.com/koron/todo-noif-mock/do"
type Service struct {
repos *Repository // Repositoryは自空間にエイリアスで定義してるのでpackageの指定は不要
}
func (srv *Service) PostTodo(task string) (*do.Todo, error) {
// なにかしら必要な手続きをしたあとに…Repository のコードを呼び出す
d, err := srv.repos.Insert(&do.Todo{Task: task})
if err != nil {
return nil, err
}
return d, nil
}
package service
import "testing"
func TestServiceCreate(t *testing.T) {
r := &Repository{
// モック用のRepositoryの初期化をここに書く
}
srv := &Service{repos: r}
d, err := srv.PostTodo("テストのタスク1")
if err != nil {
t.Fatal(err)
}
// TODO: dをチェックする
_ = d
// TODO: Insertが正しく呼ばれたか`r`の中身をチェックするなど
}
以上のようにしておけば普通のビルドはgo build ./service
で、
テストはgo test -tags mock ./service
でそれぞれ実行できるのです。
-tags mock
を忘れてテストを実行すると当然失敗することにご留意を。
ここまでのサンプルはGitHubのココを参照してください。
で、ここで使うモックをいちいち手書きしてらんねぇってことで自動生成するツールを作りました。 以下のコマンドで入手できます。
$ go get github.com/koron/mockgo
このツールは各メソッドの呼び出しパラメータを保存し戻り値を予め指定できるだけのモックを生成します。
$ cd service
$ mockgo -package ../repository Repository
$ cd ..
生成されたモックのコードは省略しますが、それを使ったservice/todo_service_test.go
は以下のように変わります。
func TestServiceCreate(t *testing.T) {
r := &Repository{
// モック用のRepositoryの初期化をここに書く
Insert_Rs: []*RepositoryInsert_R{
{&do.Todo{ID: 123, Task: "mytask1"}, nil},
},
}
srv := &Service{repos: r}
d, err := srv.PostTodo("in_task1")
if err != nil {
t.Fatal(err)
}
// dをチェックする
if !reflect.DeepEqual(d, &do.Todo{ID: 123, Task: "mytask1"}) {
t.Fatal("unexpected response")
}
// Insertが正しく呼ばれたかチェック
if !reflect.DeepEqual(r.Insert_Ps, []*RepositoryInsert_P{
{&do.Todo{Task: "in_task1"}},
}) {
t.Fatal("unexpected Insert calls")
}
}
またservice/repository.go
に次のように書いておけばgo generate ./service
するだけでモックの更新が可能になります。
//go:generate mockgo -package ../repository Repository
モックを自動生成した版のサンプルはGitHubのココを参照してください。
こうしてモックを使ってserviceとrepositoryをたくさん書いてM:Nの関係を持ち込むようになると 各serviceで自分の使わないmockを生成するのが馬鹿らしくなります。
そこで初めて各serviceで興味のあるメソッドだけを
そのサービスにおいてinterface
で定義したほうが良いと気が付くのです。
その話はまたいずれ。