[Golang]テスタブルな現在時刻取得処理を作る

# はじめに
backendサービスを作っていると大抵どこかしらで現在時刻`time.Now()`を取り扱うことになると思います。
しかし`time.Now()`の値は、刻一刻と変わる値なので`time.Now()`を使用する関数のtestを書く際には必ず
`time.Now()`の値を固定化する手段を必ず作っておかなければなりません。

Perlの場合は`Test::MockTime`、Rubyの場合は`timecop`、Pythonの場合は`FreezeGun`のようなlibraryを使ってmockしますが
Goの場合この`time.Now()`を固定化するためにどうすればよいのか幾つか方法を試したので紹介します。


# monkey.Patch()
まずは [monkey.Patch()](https://github.com/bouk/monkey)を使う方法。

**注意：一応書いておきますが、おすすめの方法ではありません。**
monkey.Patch()は実行時に実行ファイルを書き換えて、mockしたい関数を呼び出すイリーガルなツールです。
既に実装されているプロダクトコードにテストを書かなきゃいけないなど
どうしても困った時に使うという感覚で使用してください。

---

まずは、適当に`time.Now()`を使うコードを書いてみます。

```Go
package main

import (
	"context"
	"time"
)

type Hoge struct {
	Now time.Time
}

func GetHoge(ctx context.Context) *Hoge {
	now := time.Now().UTC()
	return &Hoge{
		Now: now,
	}
}
```

model用のstructに現在時刻を突っ込んで後々の処理に使うよくある実装ですね。
これのテストを`monkey.Patch()`で書いてみます。

```Go
package main

import (
	"context"
	"testing"
	"time"

	"bou.ke/monkey"
	"github.com/stretchr/testify/assert"
)

func TestGetHoge(t *testing.T) {
	t.Run("time mock test use monkey", func(t *testing.T) {
		ctx := context.Background()
		mockTime := time.Date(2020, 4, 3, 1, 2, 3, 123456000, time.UTC)
		patch := monkey.Patch(time.Now, func() time.Time { return mockTime })
		defer patch.Unpatch()
		expected := &Hoge{Now: mockTime}

		actual := GetHoge(ctx)
		assert.Equal(t, expected, actual)
	})
}
```

紹介する方法の中で使い方は一番簡単。


```Go
		patch := monkey.Patch(time.Now, func() time.Time { return mockTime })
		defer patch.Unpatch()
```

この2行を書くだけで……あら不思議！
プロダクトコードに書かれた`time.Now()`の戻り値をmockした関数の値に置き換えてくれます。
プロダクトコードを全然変更していないのに関数の挙動が変化する文字通りモンキーパッチです。
`defer patch.Unpatch()`を忘れると当該のテスト通過した後もpatchされたままになるので注意してください。

ちなみに、沢山`monkey.Patch()`を仕込みたいときは一つずつ`defer patch.Unpatch()`しなくても`defer monkey.UnpatchAll()`で全てunpatch出来ます。

```Go
		monkey.Patch(fuga.GetFuga, func() *fuga.Fuga { return &fuga.Fuga{} })
		monkey.Patch(Piyo.GetPiyo, func() *piyo.Piyo { return &piyo.Piyo{} })
		monkey.Patch(time.Now, func() time.Time { return mockTime })
		defer monkey.UnpatchAll()
```

# flextime.Now()

お次は[flextime](https://github.com/Songmu/flextime)を使う方法。

どういう発想のものなのか作者のブログ見るのが良いです。
https://songmu.jp/riji/entry/2020-01-19-flextime.html

> flextimeはテストコードの中で現在時刻を切り替えるためのライブラリです。Sleep時に実際に時間を止めずに時間が経過したように見せかける機能もあります。
> 
> つまり、PerlのTest::MockTimeやRubyのtimecop的なことをしたいわけですが、Goだとグローバルに関数の挙動を切り替えるといったことはできないため、利用にあたってはtimeパッケージで使っている関数を、flextimeパッケージに切り替える必要があります。

要するに`time.Now()`を使わずに、mock出来る独自の`flextime.Now()`を使うという方法。
見て分かる通りプロダクトコードの書き換えが必須になります。

こちらも紹介はしますが使うかどうかは自己責任でお願いします。

何故ならグローバルで定義されたmock出来る`flextime`を使う場合は
`flextime.Now()`を使っている関数Aとは別の関数Bでダミーの値をSetするような処理を作り
その関数Bで`flextime`をRestoreするのを忘れるバグがあった場合
関数Bとはまるで関係ない関数Aで取得した時刻が意図せずダミーの時刻になってしまう可能性があるからです。

---

先ほどのコードを`flextime.Now()`を使うように書き換えます。

```Go
package main

import (
	"context"
	"time"

	"github.com/Songmu/flextime"
)

type Hoge struct {
	Now time.Time
}

func GetHoge(ctx context.Context) *Hoge {
	now := flextime.Now().UTC()
	return &Hoge{
		Now: now,
	}
}
```

次にテストコード
`flextime.Set()`を使用してflextimeの内部時刻をmockの時刻に置き換えます。

```Go
package main

import (
	"context"
	"testing"
	"time"

	"github.com/Songmu/flextime"
	"github.com/stretchr/testify/assert"
)

func TestGetHoge(t *testing.T) {
	t.Run("time mock test use flextime", func(t *testing.T) {
		ctx := context.Background()
		mockTime := time.Date(2020, 4, 3, 1, 2, 3, 123456000, time.UTC)
		restore := flextime.Set(mockTime)
		defer restore()
		expected := &Hoge{Now: mockTime}

		actual := GetHoge(ctx)
		assert.Equal(t, expected, actual)
	})
}
```

現在時刻とmock時刻はflextime内で管理されているので
`flextime.Set()`するだけで`flextime.Now()`の時刻を固定化することが出来ます。

先ほどの話なのですが
`flextime.Set()`をプロダクトコード内で使用して`defer restore()`を忘れるバグがあると
全ての`flextime.Now()`を使用する関数で必ず時刻が同じになってしまうという重大なバグを生む可能性があります。


# Context.Value に現在時刻を保持する

さっきからオススメしないオススメしないばかり言って結局何をオススメするんだという話ですが
個人的にはこの「Context.Valueに現在時刻を保持する」案をオススメします。

要するに`time.Now()`を関数内で行うからmockに困るのであって
現在時刻を関数の引数で渡してしまえばいいじゃないという話。

`time.Now()`を行う場所はhandlerを呼ぶ前のmiddlewareレイヤーに移して
middleware内で`time.Now()`の結果を`Context.Value`に格納します。
そして現在時刻を使用したい時は`Context.Value`から現在時刻を取得するためのutilを使用するようにします。

---

まずは現在時刻を`context.Value`に格納したり、`context.Value`から現在時刻を取得するutilを用意します。

```Go
package timeutil

import (
	"context"
	"time"
)

const CtxFreezeTimeKey = "freeze_time_key"

// context.Valueから現在時刻を取得する関数
func Now(ctx context.Context) time.Time {
	return ctx.Value(CtxFreezeTimeKey).(time.Time)
}

// context.Valueに現在時刻を格納する関数
func SetNow(ctx context.Context) context.Context {
	return context.WithValue(ctx, CtxFreezeTimeKey, time.Now())
}

// テスト用にmockしたいtime.Timeをcontext.Valueに格納する関数
func MockNow(ctx context.Context, mockTime time.Time) context.Context {
	return context.WithValue(ctx, CtxFreezeTimeKey, mockTime)
}
```

middlewareの実装は場合によって異なるので詳しくは書きませんが
例えばgPRC Serverの場合はServer Chainに以下のようなInterceptorを挟みます。

```Go
func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (_ interface{}, err error) {
		ctx = timeutil.SetNow(ctx)
		return handler(ctx, req)
	}
}
```

次に現在時刻を実際に使用する部分

```Go
package main

import (
	"context"
	"time"

	"sample-timemock/timeutil"
)

type Hoge struct {
	Now time.Time
}

func GetHoge(ctx context.Context) *Hoge {
	now := timeutil.Now(ctx).UTC()
	return &Hoge{
		Now: now,
	}
}
```

`time.Now()`を`timeutil.Now(ctx)`に置き換えています。
`timeutil.Now(ctx)`は`flextime.Now()`とは異なり引数の`context.Context`から値を取り出すだけなので
utilで`time.Now()`をセットする処理がバグってるとか以外であれば他の関数の影響を受けることはありません。
まあそもそもutilで`time.Now()`をセットする処理がバグってる場合はutilのtestがpassしないはずです。

では上記のテストを書いていきます。

```Go
package main

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"sample-timemock/timeutil"
)

func TestGetHoge(t *testing.T) {
	t.Run("time mock test use flextime", func(t *testing.T) {
		mockTime := time.Date(2020, 4, 3, 1, 2, 3, 123456000, time.UTC)
		mockCtx := timeutil.MockNow(context.Background(), mockTime)
		expected := &Hoge{Now: mockTime}

		actual := GetHoge(mockCtx)
		assert.Equal(t, expected, actual)
	})
}
```

やってることはcontext.Valueにmockのtime.Timeを突っ込んで引数で渡しているだけで非常にシンプル。
検証してきた中ではモンキーパッチも使わずグローバルなtime領域も汚さない一番良さそうな実装だと思います。


## ・・・ただし
現在時刻をmiddlewareでセットする実装の場合は、requestでhandlerが呼ばれた時点での現在時刻をセットするため
そのスコープ内の現在時刻が必ず同じになります。

例えば、独自でログ出力処理を書いていたりする場合

```
[2020-04-03 16:10:23.134] CreateHoge()
[2020-04-03 16:10:23.134] CreateFuga()
[2020-04-03 16:10:23.134] CreatePiyo()
[2020-04-03 16:10:23.134] GetHoge()
```

みたいに全部同じ時刻になってしまうので、`time.Now()`全てを`timeutil.Now(ctx)`に置き換えるかどうかは考えた方が良いですね。