Goで現在時刻のテストどうする話(めっちゃ薄いライブラリ作った)


どういう記事?

現在時刻が絡むテストってGolangではどうするんだろうと思い調べた。
しっくりくるのがなかったので欲しい機能だけ入れたライブラリを作った。

調べた内容

@tomtwinkle さんの記事:[Golang]テスタブルな現在時刻取得処理を作る では以下3つの方法が紹介されてた。

  • monkey.Patch
  • flextimeパッケージ
  • contextで引き回す

他にはKentaro Kawanoさんの資料 も参考になった。

  • clockパッケージ
  • 外部からの注入

でも、なんか、こう、もっと簡単でいいだよなー。。。

作ったもの

って思ってめっちゃ薄いライブラリ作ってみた。
https://github.com/bubusuke/xtime

考えたこと

flextimeやclockとやろうとしていることは同じ。time.Now()をラップしてて置き換えできる。
でもこのふたつ、自分としてしっくりきてなかった。

flextimeは排他制御しててちょっと気になる。(この記事にも記載あり)
あと、flextimeって名前長い(コーディング短くしたい)。
clockはtimeと名前違いすぎてちょっと気になる。

特徴

名前:timeっぽく、でも短い名前にしよう。 → xtime
機能:絞りに絞る! → ふたつだけ。
  xtime.Now() って使うだけ。
  テストの時はxtime.Mock( ${xtime.Now()で返ってくる値} )で時間変えるだけ。
  テストの時はxtime.Mock( ${xtime.Now()で実行する関数} )で時間変えるだけ。
使い方:
  既存のソースでtime.Now()を使っている部分をこれに置き換えれば使えます。

〜〜修正:2021/01/16 初回投稿後〜〜
より柔軟に設定できるよう xtime.Mock の引数を値→関数に変えました。
これでテストケースに記載したような実行毎に時間が進むMockを作ったり、Mockをリセットしたりできるようになりました。

git公開してますが、以下にソースも載せます。


ソース

xtime.go
package xtime

import (
    "time"
)

var now func() time.Time = time.Now

// Mock overwrites return value of xtime.Now().
// You must not use this function except for in test.
func Mock(fn func() time.Time) {
    now = fn
}

// Now returns the value of time.Now().
// If the Mock function has been executed in advance, the value set by Mock is returned.
func Now() time.Time {
    return now()
}


テスト

xtime_test.go
package xtime_test

import (
    "testing"
    "time"
    "xtime"
)

type incrementalMock struct {
    i time.Duration
    t time.Time
}

func (m *incrementalMock) Now() time.Time {
    m.i++
    return m.t.Add(time.Second * m.i)
}

func TestNow(t *testing.T) {
    // case 1.
    // Default xtime.Now behavior
    // 'xtime.Now() == time.Now()' become false due to the μ second level execution time difference.
    if !(xtime.Now().Sub(time.Now()) <= time.Second*1) {
        t.Error("Default xtime.Now() must be same to time.Now().")
    }

    // case 2.
    // Constant value Mock
    mockTime := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
    xtime.Mock(func() time.Time { return mockTime })
    if xtime.Now() != mockTime {
        t.Error("xtime.Now() must be same to MockTime.")
    }

    // case 3.
    // Incremental value Mock
    incMock := &incrementalMock{
        i: 0,
        t: mockTime,
    }
    xtime.Mock(incMock.Now)
    if xtime.Now().Sub(mockTime) != time.Second*1 {
        t.Error("xtime.Now() must be same to MockTime+1sec.")
    }
    if xtime.Now().Sub(mockTime) != time.Second*2 {
        t.Error("xtime.Now() must be same to MockTime+2sec.")
    }

    // case 4.
    // reset
    xtime.Mock(time.Now)
    // Same to 1st test case.
    if !(xtime.Now().Sub(time.Now()) <= time.Second*1) {
        t.Error("Default xtime.Now() must be same to time.Now().")
    }

}