Goテストの綺麗な書き方

eureka, Inc.
Eureka Engineering
Published in
8 min readApr 21, 2017

こんにちは。イギリス出身のJamesです。


RubyとJavaみたいな以前から使われている言語はテストのライブラリーが多く、テスト方法の例も多くあります。逆に、Goのような割と新しい言語はベスト・プラクティスがあまりないし、よく使われるライブラリーでもスピードが遅くあまり綺麗じゃないテストをよく見かけます。今回は、どこでも使える綺麗なテスト方法をご紹介します。


ほとんどの現代のアーキテクチャーはある程度レイヤーを使います。最近流行っているDomain Driven Designはだいたいの複雑な処理をドメインレイヤーのサービスにまとめています。なので、ドメインレイヤーはとても大切です。ドメインレイヤーも一番テストしやすいレイヤーなので、そこにフォーカスします。


エウレカはTestifyというライブラリーを使っていますが、どんなフレームワークでも基本は同じです:

  1. レイヤーの依存はstructではなく、interfaceとして定義(Object Oriented Designの大事なDependency Inversion Principle)
  2. テスト環境で依存を本当のオブジェクトのかわりにモックを使用
  3. ユニットテストが簡単に書けるように、小さくて循環性の複雑さが低い関数を書く

テストしやすいレポジトリ

例えば、レシピを表示するサイトを作ります。ここでは、ユーザーIDを使ってユーザーが提出したレシピを取得したいです。このように定義されているレポジトリがあります。

type RecipeRepositoryImpl struct {
rootRepository RootRepository
}
func (r *RecipeRepositoryImpl) GetRecipesByUserID(userID int) []Recipe {
// DB アクセス
return recipes
}
レポジジトリをアクセスするサービスに直接RecipeRepositoryImplに書くと、本質の関数を使うのでモックできません。そのかわりに、interfaceを定義します。type RecipeRepository interface {
GetRecipesByUser(userID int) []Recipe
}
サービスからレポジトリを開始するとき、RecipeRepositoryタイプを返したいけど、中身はDBに接続するstructにしなければいけません。そ子で、New関数をこのように定義します。func NewRecipeRepository() RecipeRepository {
r := RecipeRepositoryImpl {
rootRepository: NewRootRepository()
}
var rep RecipeRepository = &r
return rep
}
モック次はTestifyを使ってモックを作成します。Testifyのモックはちょっと変わったボイラープレートが必要です。大きいstructならモックも長くなるので普通に別のファイルで作ります。import “github.com/stretchr/testify/mock”type RecipeRepositoryMock {
mock.Mock
}
func (m *RecipeRepositoryMock) GetRecipesByUserID(userID int) []Recipe {
args := m.Called(userID)
return args.Get(0).([]Recipe)
}
interfaceとして使えるようにRecipeRepositoryImplと同じ関数を定義しなければなりません。モックは普通にパラメーター次第にバリューを返します。args… からの行は「このパラメータ次第レターンして、他のパラメータを無視」ということを意味しています。


コードを書きながらこのボイラープレートを書くのは面倒ではありませんが、以前から定義されている大きいクラスをモックしたいなら、全ての関数を書くのは非常に大変です。簡単にできるため、スクリプトを作ったので自由に使ってください。
https://github.com/jamesneve/testify_plumber(生成されるコードは微妙に間違っている時があるので、あとで手動で修正する必要があります)モックが使えるサービス普通にサービスの上にApplication LayerあるいはPresentation Layerがあるので、サービスもモックできるようにinterfaceとして定義したいのですが、簡単に説明できるように今回はstructだけで定義します。サービスもユーザーIDでレシピを取得します。もしなければ、エラーを返します。


(名詞でサービスに名前をつける人が多いですが、Eric Evans (DDDを元々考えた方)によるとは動詞で名前をつけた方が良いです。なぜかというと、サービスはものじゃなくて、「何かをする」タイプですから。)
type GetRecipesServiceImpl struct {
recipeRepository RecipeRepository
}
func NewGetRecipesService() GetRecipesServiceImpl {
return GetRecipesServiceImpl {
recipeRepository: repositories.NewRecipeRepository()
}
}
func (s *GetRecipesServiceImpl) GetRecipesByUser(userID int) ([]Recipe, error) {
recipes := s.recipeRepository.GetRecipesByUserID(userID)
// 普通に空っぽだけでエラー返さないけど、テストできる関数を作りたかった、、、
if len(recipes) == 0 { return recipes. errors.New(“No recipes for user”) }
return recipes, nil
}
テスト今、依存まで行かずにサービステストできます。GetRecipesByUserは一回ブランチするので、両方のパスをテストしたいです。そのために、レポジトリのモックをUserIDによってい空っぽか空っぽじゃないレシピスライスを返すように設定できます。func TestGetRecipesService_GetRecipesByUser(t *testing.T) {
assert := assert.New(t)
recipeRepositoryMock := new(repositories.RecipeRepositoryMock)
s := GetRecipesServiceImpl {
recipeRepository: recipeRepositoryMock,
}
// レシピがある場合はエラーなしでレターン
returnValue := []Recipie{{}, {}}
recipeRepositoryMock.On("GetRecipesByUserID", 1).Return(returnValue)
result, err := s.GetRecpiesByUser(1)
assert.Equal(returnValue, result)
assert.NoError(err)
// レシピが倍場合はエラーを返す
returnValue = []Recipe{}
recipeRepositoryMock.On("GetRecpiesByUserID", 2).Return(returnValue)
result, err = s.GetRecpiesByUser(2)
assert.Equal(returnValue, result)
assert.Error(err)
}
引数パターンとモックレスポンスは1対1なので、二つのテストを書くため、モックは「1」と「2」のレスポンスを定義します。もし引数がない関数をモックしなければいけない場合、モックとサービスは数回開始しなくてはなりません。おわりに真ん中のレイヤー(ドメインサービス、アプリケーションサービスなど)は全部このパターンで使えます。早いレポジトリテストを作りたいならデータレイヤーをモックしなければいけません。これは割と複雑でReflectionも使わなければいけないので、また別のポストでご紹介します。


レイヤーを綺麗に分けて、小さくて循環性の複雑さが低い関数を書いて、モックを使えば、綺麗なテストを早く簡単に書けるようになります!

--

--

eureka, Inc.
Eureka Engineering

Learn more about how Eureka technology and engineering