この記事は and factory Advent Calendar 2020 の22日目の記事です。
私のチームでは、DIをする際にgoogle/wire
を使用しているのですが、モックの使い方について調べたところ、公式のGitHubにて、ベストプラクティスのページに記載がありましたので、それを日本語で解説しようと思います。
なお、本記事ではgoogle/wire
そのものの説明はしません。こちらに詳しく紹介されていますのでそちらを参照してみてください。
公式ではモックを注入するためのアプローチとして2つ紹介されていますので、両方について解説します。
モックを追加する前のサンプルソース
解説を始める前にモックなしの状態のサンプルソースを載せておきます。
サイコロを振って旅の行き先を決めるという簡単なサンプルソースを作成しました。
package main
import (
"fmt"
"math/rand"
"strconv"
"time"
)
// App アプリケーション
type App struct {
DiceTrip DiceTrip
}
// DiceTrip サイコロの旅
type DiceTrip struct {
Dice Dice
}
// Decide 旅の行き先を決める
func (dt DiceTrip) Decide() string {
n := dt.Dice.Roll()
str := strconv.Itoa(n) + ": "
if n == 6 {
str += "深夜バスで福岡へ"
} else {
str += "飛行機で新千歳空港へ"
}
return str
}
// Dice サイコロ
type Dice interface {
Roll() int
}
// RealDice Diceインタフェースを実装したもの
type RealDice struct {
}
// Roll サイコロを振る(擬似乱数を使用)
func (ds RealDice) Roll() int {
return rand.Intn(6) + 1
}
func main() {
rand.Seed(time.Now().UnixNano())
app := InitApp()
fmt.Println(app.DiceTrip.Decide())
}
//+build wireinject
package main
import "github.com/google/wire"
// InitApp Appを生成する
func InitApp() *App {
wire.Build(
wire.Struct(new(App), "*"),
wire.Struct(new(DiceTrip), "*"),
wire.InterfaceValue(new(Dice), RealDice{}),
)
return nil
}
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package main
// Injectors from wire.go:
// InitApp Appを生成する
func InitApp() *App {
dice := _wireRealDiceValue
diceTrip := DiceTrip{
Dice: dice,
}
app := &App{
DiceTrip: diceTrip,
}
return app
}
var (
_wireRealDiceValue = RealDice{}
)
% go run main.go wire_gen.go
3: 飛行機で新千歳空港へ
なぜモックを注入するアプローチは通常と異なるのか
実際に解説に入る前に、モックを注入するために特別なことをしなければいけない理由について述べておきます。
テストでモックを使用する場合、テストを実施する前に、モックがすべき動作をあらかじめ指定しておく必要があるはずです。
ですが、通常と同様にwire.Build
内でモックを生成して返してしまうと、当然インタフェースとして返ってきますのでモックを直接操作することができません。
よって、モックを注入する場合、以下の両方の条件を満たす必要があります。
- Injectorの呼び出し元でモックを直接操作できる
- 上記モックは、Injector内で生成された構造体にセットされる
サンプルソースの場合、DiceTrip.Decide
の出力結果をテストするのであれば、Dice.Roll
の結果をランダムではなく、特定の値が返ってくるようにモックを作成したいです。
理由を整理したので、改めてモックを注入するためのアプローチ2つについて解説していきます。
モックをInjectorに渡す
1つ目のアプローチは、モックを直接Injectorに渡す方法です。
モックの生成をInjector内ではなく、Injectorの呼び出し元で生成してしまえば問題なくモックを直接操作できるようになります。
また、テスト用のInjectorを新規作成します。wire.Build
ではモックのProviderは定義せず、Injectorの引数経由でモックを受け取るようにします。
以下、サンプルソースです。(変更部分のみ抜粋)
...
// MockedDice Diceインタフェースを実装したモック
type MockedDice struct {
N int
}
// NewMockedDice MockedDiceを生成する
func NewMockedDice() *MockedDice {
return &MockedDice{}
}
// Roll サイコロを振る
func (md MockedDice) Roll() int {
return md.N
}
func main() {
rand.Seed(time.Now().UnixNano())
d := NewMockedDice()
app := InitMockedAppFromArgs(d) // 生成したモックを渡す
d.N = 5
fmt.Println(app.DiceTrip.Decide())
d.N = 6
fmt.Println(app.DiceTrip.Decide())
}
...
func InitMockedAppFromArgs(d Dice) *App {
wire.Build(
wire.Struct(new(App), "*"),
wire.Struct(new(DiceTrip), "*"),
)
return nil
}
...
func InitMockedAppFromArgs(d Dice) App {
diceTrip := DiceTrip{
Dice: d,
}
app := App{
DiceTrip: diceTrip,
}
return app
}
% go run main.go wire_gen.go
5: 飛行機で新千歳空港へ
6: 深夜バスで福岡へ
Injectorでモックを生成し返す
2つ目のアプローチは、Injector内で生成したモックを返し、Injectorの呼び出し元で操作できるようにする方法です。
このアプローチの場合は、アプリとモックを含む構造体を新規に作成します。
この構造体を返すようなテスト用Injectorを作成し、それを使用します。
以下、サンプルソースです。(変更部分のみ抜粋)
...
// AppWithMocks アプリケーションとアプリケーション内に注入されたモックを格納した構造体。テストで使用
type AppWithMocks struct {
App App
MockedDice *MockedDice
}
...
// MockedDice Diceインタフェースを実装したモック
type MockedDice struct {
N int
}
// NewMockedDice MockedDiceを生成する
func NewMockedDice() *MockedDice {
return &MockedDice{}
}
// Roll サイコロを振る
func (md MockedDice) Roll() int {
return md.N
}
func main() {
rand.Seed(time.Now().UnixNano())
appWithMocks := InitMockedApp()
appWithMocks.MockedDice.N = 5
fmt.Println(appWithMocks.App.DiceTrip.Decide())
appWithMocks.MockedDice.N = 6
fmt.Println(appWithMocks.App.DiceTrip.Decide())
}
...
func InitMockedApp() *AppWithMocks {
wire.Build(
wire.Struct(new(App), "*"),
wire.Struct(new(DiceTrip), "*"),
wire.Struct(new(AppWithMocks), "*"),
NewMockedDice,
wire.Bind(new(Dice), new(*MockedDice)), // DiceとMockedDiceを紐付ける
)
return nil
}
...
func InitMockedApp() *AppWithMocks {
mockedDice := NewMockedDice()
diceTrip := DiceTrip{
Dice: mockedDice,
}
app := App{
DiceTrip: diceTrip,
}
appWithMocks := &AppWithMocks{
App: app,
MockedDice: mockedDice,
}
return appWithMocks
}
% go run main.go wire_gen.go
5: 飛行機で新千歳空港へ
6: 深夜バスで福岡へ
2つのアプローチのメリット・デメリット
1つ目の方式の場合、テスト用Injectorの定義が2つ目と比べると記述量が少なくて済むのは良いのですが、Injectorの呼び出し元で生成処理を書かなければならないというデメリットも存在します。
2つ目は、1つ目のデメリットは解消しているのですが、テスト用Injectorの定義が1つ目と比べて若干記述量が多い上に、テスト用の構造体を新規で作成する必要があります。
個人的には2つ目のアプローチを採用しています。
せっかく生成処理をwire.go
に寄せたのに、生成処理が一部呼び出し元に流出するのは避けたいです。
もし、記述量が増えるのが気になるのであれば、構造体やwire.go
を自動で生成してしまうという手もあります。自動で生成するのであれば、そもそも記述量を気にする必要もなくなりますね。
終わりに
google/wire
について調べていた時に、テスト用のモックの注入まで述べている日本語の記事が思ったより少なかったので今回解説させていただきました。何かしらのお役に立てていたら幸いです。