wireを使ったプロジェクトでモックを用いたAPIのテストを書く

この記事は [カオナビ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/kaonavi) 12日目です。

# はじめに

社内のAPI開発ではWebフレームワークとして [gin](https://github.com/gin-gonic/gin)、DIを行うために [wire](https://github.com/google/wire) を使用しています。
（gin, wireについては割愛します）

API開発をするプロジェクトでは`httptest`パッケージを使用したAPIのテストを重点的に書くようにしていますが、
wireを導入した際に`time`パッケージ等をモック化したテストがうまくいかず苦戦しました。

今回はAPI開発のプロジェクトにwireを導入した時に、モックを使ってテストを書けるようにする方法を書いてみます。

# 課題

wireの[Best Practices](https://github.com/google/wire/blob/main/docs/best-practices.md#mocking)にはMockingについての記述があります。
書かれている通りに実装するとテスト専用のインジェクターを作成する必要があり、さらにテスト専用のルーティングも必要になってしまいました。

ルーティングは本番稼働するものと全く同じものを使用したかったため、Best Practicesの方法だと要求を満たせませんでした。

# モック化の手法

今回はwireの[Best Practices](https://github.com/google/wire/blob/main/docs/best-practices.md#mocking)の方法も含めて3パターンの手法でモック化をしていきます。
Approach A/Bは[Best Practices](https://github.com/google/wire/blob/main/docs/best-practices.md#mocking)のMockingにある手法を参考にしています。
もちろん他にも手法はあると思いますので一例としてみていただければと思います。

1. Approach A
    - モックを引数として受け取るテスト専用のインジェクターを作成する手法
1. Approach B
    - アプリと、モックしたいすべての依存関係を含む新しい構造体を作成し、この構造体を返す専用のインジェクターを作成する手法
1. Approach C
    - wireに依存関係を返すコンストラクタを直接設定しない
    - 依存関係を返す関数を一つ挟んであげるという手法です。
    - **現在はこのやり方を採用しています**

# まとめ

以降はサンプルコードで長くなるので先にまとめを書いておきます。

現在は`Approche C`の方法を用いてテストを書いていて、モック化しているinterfaceが10個ほどあります。
モック化の対象をtimeパッケージなど実行ごとに結果が変わってしまうもの、外部のAPIを実行するコードに絞ることで対象が増えすぎないようにしています。
10個でも多いとは感じていますが、APIごとにモック化が必要なinterfaceは1,2個なので今の所はそれほど苦では無いです。

Approach A/Bではテスト用のインジェンクターを作る + テスト用のルーティングを作る必要があるという点が気になったので使用していません。
（やり方を工夫すればテスト用のインジェクターを作らなくてもいけそうですが）

**他にいいやり方があれば是非教えていただきたいです!!**

---

# ここからはサンプルコードです

## ベースになるAPIとテストの作成

まずはgin + wireを使って動く簡単なAPIとAPIのテストを作ります。
今回は構成はこのような形で作成します。

```
.
├── app
│   ├── handler.go
│   ├── route.go
│   ├── timer.go
│   ├── wire.go
│   └── wire_gen.go
├── main.go
└── test
    └── api_test.go
```

### main.go

ここでは`gin.Engine.Run()`を呼び出すだけです。

```go
package main

import "adcal/app"

func main() {
	if err := app.Route().Run(); err != nil {
		panic(err)
	}
}
```

### app/route.go

`gin.Engine`の初期化はPublicメソッドとして切り出します。
`main.go`から呼び出しますし、後述のテストからも使用するためです。
こうすることでAPIのテストも実際のルーティングと全く同じ状態でテストすることが出来ます。

```go
package app

import (
	"github.com/gin-gonic/gin"
)

func Route() *gin.Engine {
	e := gin.New()
	e.GET("sample", InitializeHandler().Run)
	return e
}
```

### app/timer.go

`time`パッケージを直接使うと日付に依存してしまうため、interfaceを用意してtimeパッケージを直接使用しないようにしています。
interface化しておくことで後でモックに差し替えが可能になります。

```go
package app

import "time"

type Timer interface {
	Now() time.Time
}

func NewTimer() Timer {
	return &timer{}
}

type timer struct{}

func (t *timer) Now() time.Time {
	loc,_:=time.LoadLocation("Asia/Tokyo")
	return time.Now().In(loc)
}
```

### app/handler.go

現在時刻を返すだけのAPIです。
コンストラクタを用意してTimerのinterfaceを受け取れるようにしておきます。

```go
package app

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

type Handler struct {
	t Timer
}

func NewHandler(t Timer) *Handler {
	return &Handler{t}
}

func (c *Handler) Run(ctx *gin.Context) {
	ctx.JSON(http.StatusOK, gin.H{
		"now": c.t.Now().Format("2006-01-02 15:04:05"),
	})
}
```

### app/wire.go

wireでDIするための設定です。
`wire.Bind`にはHandler, Timerのコンストラクタを渡します。
この関数はnilを返却してしまって問題ありません。

```go
//go:build wireinject
// +build wireinject

package app

import "github.com/google/wire"

func InitializeHandler() *Handler {
	wire.Build(NewHandler, NewTimer)
	return nil
}
```

### app/wire_gen.go

ここまでコードを作成し`wire`コマンドを実行すると出力されるファイルです。

```go
// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package app

// Injectors from wire.go:

func InitializeHandler() *Handler {
	appTimer := NewTimer()
	handler := NewHandler(appTimer)
	return handler
}
```

### test/api_test.go

APIのテストは`httptest`パッケージを使用して実装します。
`gin.Engine`は`http.Handler` interfaceを満たすため、`httptest.NewServer()`に渡しててあげるだけでOKです。

レスポンスボディの現在日は `2021-12-12 12:00:00` を返すことを期待するようにしています。
そのためこのテストは2021年12月12日 12時00分00秒に実行しない限りは必ず失敗します。

```go
package app_test

import (
	"adcal/before/app"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
)

func Test(t *testing.T) {
	gin.SetMode(gin.TestMode)

	serv := httptest.NewServer(app.Route())
	req, err := http.NewRequest(http.MethodGet, serv.URL+"/sample", nil)
	if err != nil {
		t.Fatal(err)
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		t.Errorf("status returns %d, want %d", resp.StatusCode, http.StatusOK)
	}

	b, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatal(err)
	}

	e := `{"now":"2021-12-12 12:00:00"}`
	a := string(b)
	if a != e {
		t.Errorf("body returns %s, want %s", a, e)
	}
}
```

**実行結果**

```sh
$ go test -v ./test/...
=== RUN   Test
    api_test.go:41: body returns {"now":"2021-12-05 22:32:43"}, want {"now":"2021-12-12 12:00:00"}
--- FAIL: Test (0.00s)
FAIL
FAIL    adcal/before/test       0.122s
FAIL
```

現時点ではTimerをモック化できるようになっていないためテストは失敗します。
このコードからTimerをモックに置き換えてテストできるように修正していきます。

---

## Approach A

wireの[Best Practices](https://github.com/google/wire/blob/main/docs/best-practices.md#mocking)に記載されているApproach Aの方法でテストしてみます。

モックを引数として受け取るテスト専用のインジェクターを作成する、という手法になります。

> Approach A: Pass mocks to the injector
>
> Create a test-only injector that takes all of the mocks as arguments; the argument types must be the interface types the mocks are mocking. wire.Build can't include providers for the mocked dependencies without creating conflicts, so if you're using provider set(s) you will need to define one that doesn't include the mocked types.

### 1. テスト専用のインジェクターを作成する

`wire.go`に`MockInitializeHandler()`を追加し、Timer interfaceを引数から受け取るようにします。

```go
//go:build wireinject
// +build wireinject

package app

import "github.com/google/wire"

func InitializeHandler() *Handler {
	wire.Build(NewHandler, NewTimer)
	return nil
}

// Timerを引数に受け取るイジェクターを追加します
func InitializeMockHandler(mt Timer) *Handler {
	wire.Build(NewHandler)
	return nil
}
```

この状態で`wire`を実行すると以下のファイルが生成されます。

```go
// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package app

// Injectors from wire.go:

func InitializeHandler() *Handler {
	appTimer := NewTimer()
	handler := NewHandler(appTimer)
	return handler
}

func InitializeMockHandler(mt Timer) *Handler {
	handler := NewHandler(mt)
	return handler
}
```

### 2. テスト専用のルーティングを作成

作成済みの`Route()`関数にモック用のインジェクターを差し込むことは出来ないので
テスト専用のルーティングメソッドとして`TestRoute()`を用意します。
`InitializeMockHandler()`に渡すTimer interafaceを引数で受け取る必要があります。

```go
package app

import (
	"github.com/gin-gonic/gin"
)

func Route() *gin.Engine {
	e := gin.New()
	e.GET("sample", InitializeHandler().Run)
	return e
}

func TestRoute(mt Timer) *gin.Engine {
	e := gin.New()
	e.GET("sample", InitializeMockHandler(mt).Run)
	return e
}
```

### 3. テストの修正

Time interfaceを満たすmockTimerを用意し、固定の日付を返却できるようにします。
`httptest.NewServer()`には先ほど作成したテスト用の`TestRoute()`の戻り値を設定します。

```go
type mockTimer struct{}

func (m *mockTimer) Now() time.Time {
	loc, _ := time.LoadLocation("Asia/Tokyo")
	return time.Date(2021, time.December, 12, 12, 00, 00, 0, loc)
}

func TestApproachA(t *testing.T) {
	gin.SetMode(gin.TestMode)

	// TestRouteにTimerのモックを渡します
	serv := httptest.NewServer(app.TestRoute(&mockTimer{}))
	req, err := http.NewRequest(http.MethodGet, serv.URL+"/sample", nil)

	// ... 省略
}
```

**実行結果**

```sh
$ go test -v ./test/...
=== RUN   TestApproachA
--- PASS: TestApproachA (0.00s)
PASS
ok      adcal/approach/a/test   0.296s
```

### まとめ

テスト専用のインジェクターを毎回作るのはちょっと面倒くさいのと
テスト用のルーティングも用意する必要があるため、 `app.Route()`をそのまま使えないのがちょっと気になります。

またサンプルコードの作りだと、モック化したい依存関係が増えた時に`TestRoute()`の引数が多くなるので扱いにくくなってしまいます。

TestRouteの引数を無くし、`testhelper`パッケージ等を作って内部でモックを取得したり、
依存関係を構造体にまとめたものをTestRouteの引数にしたりすると多少扱いやすくはなりそうですが。

```go
func TestRoute() *gin.Engine {
	e := gin.New()
	mt := testhelper.NewMockTimer()
	e.GET("sample", InitializeMockHandler(mt).Run)
	return e
}

func TestRoute(d Depends) *gin.Engine {
	e := gin.New()
	e.GET("sample", InitializeMockHandler(d.Timer).Run)
	return e
}
```

---

## Approach B

wireの[Best Practices](https://github.com/google/wire/blob/main/docs/best-practices.md#mocking)に記載されているApproach Bの方法でテストしてみます。

アプリと、モックしたいすべての依存関係を含む新しい構造体を作成し、この構造体を返す専用のインジェクターを作成する、という手法になります。

> Approach B: Return the mocks from the injector
>
> Create a new struct that includes the app plus all of the dependencies you want to mock. Create a test-only injector that returns this struct, give it providers for the concrete mock types, and use wire.Bind to tell Wire that the concrete mock types should be used to fulfill the appropriate interface.

### 1. 依存関係を含む新しい構造体を作成する

`wire_b.go`を新規作成し、依存関係を含む構造体を追加します。
`MockHandler.Timer`にはモック化したTimer interfaceを埋め込めるようにしておく必要があります。
またテストで日付を変更できるようにするために`time.Time`をPublic変数として定義しておきます。

```go
package app

import "time"

var MT time.Time

type MockHandler struct {
	Handler *Handler
	Timer   *MockTimer
}

type MockTimer struct{}

func NewMockTimer() *MockTimer {
	return &MockTimer{}
}

func (m *MockTimer) Now() time.Time {
	return MT
}
```

さらに`wire.go`にMockHandlerを返すインジェクターを追加します。

```go
//go:build wireinject
// +build wireinject

package app

import "github.com/google/wire"

func InitializeHandler() *Handler {
	wire.Build(NewHandler, NewTimer)
	return nil
}

func InitializeMockHandler() *MockHandler {
	wire.Build(
		wire.Struct(new(MockHandler), "*"),
		NewHandler,
		newMockTimer,
		wire.Bind(new(Timer), new(*MockTimer)),
	)
	return nil
}
```

この状態で`wire`を実行すると以下のファイルが生成されます。

```go
// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package app

// Injectors from wire.go:

func InitializeHandler() *Handler {
	appTimer := NewTimer()
	handler := NewHandler(appTimer)
	return handler
}

func InitializeMockHandler() *MockHandler {
	mockTimer := newMockTimer()
	handler := NewHandler(mockTimer)
	mockHandler := &MockHandler{
		Handler: handler,
		Timer:   mockTimer,
	}
	return mockHandler
}
```

### 2. テスト専用のルーティングを作成

作成済みの`Route()`関数は使用できないため
テスト専用のルーティングメソッドとして`TestRoute()`を用意します。

```go
package app

import (
	"github.com/gin-gonic/gin"
)

func Route() *gin.Engine {
	e := gin.New()
	e.GET("sample", InitializeHandler().Run)
	return e
}

func TestRoute() *gin.Engine {
	e := gin.New()
	e.GET("sample", InitializeMockHandler().Handler.Run)
	return e
}
```

### 3. テストの修正

`httptest.NewServer()`には先ほど作成したテスト用の`TestRoute()`の戻り値を設定します。
`app.MT`にテスト用の日付を入れておきます

```go
func TestApproachB(t *testing.T) {
	gin.SetMode(gin.TestMode)

	loc, _ := time.LoadLocation("Asia/Tokyo")
	app.MT = time.Date(2021, time.December, 12, 12, 00, 00, 0, loc)

	serv := httptest.NewServer(app.TestRoute())
	req, err := http.NewRequest(http.MethodGet, serv.URL+"/sample", nil)

	// ... 省略
}
```

**実行結果**

```sh
$ go test -v ./test/...
=== RUN   TestApproachB
--- PASS: TestApproachB (0.00s)
PASS
ok      adcal/approach/b/test   0.207s
```

### まとめ

このパターンでもテスト専用のインジェクターを毎回作る必要がありますが、Approach Aよりも複雑なインジェクターを作らなければなりません。
そしてApproach Aと同様にテスト用のルーティングを用意する必要がある点が気になりました。

---

## Approach C

これはwireのBest Practicesにはない手法になります。

依存関係を返すコンストラクタを実行する関数を作成し、コンストラクタ自体を書き換えるという方法になります。

### 1. 依存関係を返す関数を用意する

`wire_c.go`として以下のファイルを追加します。

まずはパッケージのPublic変数として`InitializeTimerFunc`を定義し、値に依存関係（今回はTimer interface）を返すコンストラクタを設定します。
（`InitializeTimerFunc`は`func() Timer`型の変数となります）

次に`InitializeTimerFunc`の実行結果を返す`InitializeTimer()`を追加します。

デフォルト値として`NewTimer`を設定しているため、テスト以外ではtimer.goに作った現在日時を返すTimerが使用されます。

```go
package app

var InitializeTimerFunc func() Timer = NewTimer

func InitializeTimer() Timer {
	return InitializeTimerFunc()
}
```

### 2. wireの設定を変更する

`wire.Bind`には`NewTimer`の代わりに、先程追加した`InitializeTimer`関数を設定します。

```go
//go:build wireinject
// +build wireinject

package app

import "github.com/google/wire"

func InitializeHandler() *Handler {
	wire.Build(NewHandler, InitializeTimer)
	return nil
}
```

この状態で`wire`を実行すると以下のファイルが生成されます。
`InitializeTimer()`の実行結果を`NewHandler()`に渡す、という挙動に変わります

```go
// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package app

// Injectors from wire.go:

func InitializeHandler() *Handler {
	appTimer := InitializeTimer()
	handler := NewHandler(appTimer)
	return handler
}
```

### 3. ルーティングの修正

このパターンではルーティングには一切手を加えません。

### 4. テストの修正

ここでもTime interfaceを満たすmockTimerを用意し、固定の日付を返却できるようにします。

`app.InitializeTimerFunc`にmockTimerを返す関数を設定します。

`app.InitializeTimerFunc`を上書きするので念の為`defer`で前の状態に戻せるようにしておくと良いと思います。

ルーティングは変更してないので`httptest.NewServer()`には`app.Route()`の戻り値を設定できます。

```go
type mockTimer struct{}

func (m *mockTimer) Now() time.Time {
	loc, _ := time.LoadLocation("Asia/Tokyo")
	return time.Date(2021, time.December, 12, 12, 00, 00, 0, loc)
}

func TestApproachC(t *testing.T) {
	gin.SetMode(gin.TestMode)

	old := app.InitializeTimerFunc
	defer func() { app.InitializeTimerFunc = old }()
	// InitializeTimerFuncにTimerのモックを返すコンストラクタを注入します
	app.InitializeTimerFunc = func() app.Timer { return &mockTimer{} }

	serv := httptest.NewServer(app.Route())
	req, err := http.NewRequest(http.MethodGet, serv.URL+"/sample", nil)

	// ... 省略
}
```

**実行結果**

```sh
$ go test -v ./test/...
=== RUN   TestApproachC
--- PASS: TestApproachC (0.00s)
PASS
ok      adcal/approach/c/test   0.232s
```

### まとめ

このパターンではコンストラクタをラップする手間が増えますが
インジェクターの追加なし、`app.Route()`の変更なしでAPIをテストを実行することができました。

モック化したいコードが増えた場合は`var InitializeXxxFunc`と`func InitializeXxx()`のセットが増えていきます。