Goでテスタブルに現在日時を扱いたい
アプリケーションを開発していると現在日時を使いたい場面に遭遇します。
このような場面で time.Now() の出力はその時々で変化していくため、テストを書く際に工夫が必要です。
例えば、JavaScriptのテストライブラリJestでは以下のように実行時の日時を固定化する モック機構 が提供されています。
jest.useFakeTimers(); jest.spyOn(global, 'setTimeout'); test('waits 1 second before ending the game', () => { const timerGame = require('../timerGame'); timerGame(); expect(setTimeout).toHaveBeenCalledTimes(1); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); });
しかし、Goの標準ライブラリではこのような機構が用意されていません。
今回は Go の Context を利用することでテスタブルに time.Now() を取り扱えるのではないか?と考えたので検証してみました。
検証
日時をContextに詰めたり、Contextから取り出すためのutilを用意します。
package timeUtil import ( "context" "time" ) type TimeKey string const Key TimeKey = "now" func GetNow(ctx context.Context) time.Time { return ctx.Value(Key).(time.Time) } func SetNow(ctx context.Context, now time.Time) context.Context { return context.WithValue(ctx, Key, now) }
利用の仕方は以下のイメージ。サンプルとして昨年を取得する処理を書いてみます。Webアプリケーションなら middleware で context に time.Now() をセットして後続のハンドラにリレーしていきます。
package main import ( "context" "time" "example.com/timeUtil" ) func GetLastYear(ctx context.Context) int { now := timeUtil.GetNow(ctx) return now.AddDate(-1, 0, 0).Year() } func main() { ctx := context.Background() // 本来はmiddlewareなどでcontextにtime.Now()をセットする ctxWithNow := timeUtil.SetNow(ctx, time.Now()) println(GetLastYear(ctxWithNow)) }
テストを書きます。モックの日時を用意し context にセットします。テストしたい処理の引数にモックした context を渡せばOK。
package main import ( "context" "testing" "time" "example.com/timeUtil" ) func Test_GetLastYear(t *testing.T) { t.Run("正常系", func(t *testing.T) { mockTime := time.Date(2024, 1, 10, 13, 10, 10, 10, time.UTC) mockCtx := timeUtil.SetNow(context.Background(), mockTime) actual, expected := GetLastYear(mockCtx), 2023 if actual != expected { t.Errorf("got %v\nwant %v", actual, expected) } }) }
テストがpassしました。
調べてみると自前でライブラリを作っている人もいますが、context を利用する方法がシンプルでやりやすいのではないかと思います。