3歩進んで2歩下がる

Software Engineer

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 を利用する方法がシンプルでやりやすいのではないかと思います。