19
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

and factoryAdvent Calendar 2020

Day 22

GoのDIライブラリ google/wire でモックを使う場合のベストプラクティス

Last updated at Posted at 2020-12-21

この記事は and factory Advent Calendar 2020 の22日目の記事です。

私のチームでは、DIをする際にgoogle/wireを使用しているのですが、モックの使い方について調べたところ、公式のGitHubにて、ベストプラクティスのページに記載がありましたので、それを日本語で解説しようと思います。
なお、本記事ではgoogle/wireそのものの説明はしません。こちらに詳しく紹介されていますのでそちらを参照してみてください。

公式ではモックを注入するためのアプローチとして2つ紹介されていますので、両方について解説します。

モックを追加する前のサンプルソース

解説を始める前にモックなしの状態のサンプルソースを載せておきます。
サイコロを振って旅の行き先を決めるという簡単なサンプルソースを作成しました。

main.go
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())
}

wire.go
//+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
}
wire_gen.go
// 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の引数経由でモックを受け取るようにします。

以下、サンプルソースです。(変更部分のみ抜粋)

main.go
...

// 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())
}
wire.go
...

func InitMockedAppFromArgs(d Dice) *App {

	wire.Build(
		wire.Struct(new(App), "*"),
		wire.Struct(new(DiceTrip), "*"),
	)

	return nil
}
wire_gen.go
...

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を作成し、それを使用します。

以下、サンプルソースです。(変更部分のみ抜粋)

main.go
...

// 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())
}

wire.go
...

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
}
wire_gen.go
...

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について調べていた時に、テスト用のモックの注入まで述べている日本語の記事が思ったより少なかったので今回解説させていただきました。何かしらのお役に立てていたら幸いです。

19
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?