テスト書く時とかに便利

package main

import (
	"fmt"
	"sync"
	"time"
)

// Layouts Ymd ってなんだっけ ... ってなるので
const (
	YmdLayout = "2006-01-02 15:04:05"
)

// エラー作成共通化
func errNotFound(uid string) error {
	return fmt.Errorf("time: not found uid=%s", uid)
}

// TimeUtil 時間操作系
type TimeUtil struct {
	times map[string]time.Time // uid 毎の time を持つ

	cond *sync.Cond // 共有したリソースが操作されるため
}

// DefaultTimeUtil TimeUtil のデフォルト
var DefaultTimeUtil *TimeUtil

// InitializeTimeUtil DefaultTimeUtil の初期化
func InitializeTimeUtil() {
	DefaultTimeUtil = &TimeUtil{
		times: map[string]time.Time{},
		cond:  sync.NewCond(&sync.Mutex{}),
	}
}

// Time DefaultTimeUtil のエイリアス
func Time() *TimeUtil {
	return DefaultTimeUtil
}

// Start UIDに対する時間の開始。 format は Ymd 形式。format が空なら現在時刻を設定。
func (tu *TimeUtil) Start(uid string, format string) (err error) {
	tu.cond.L.Lock()
	defer tu.cond.L.Unlock()

	var t time.Time
	if format == "" {
		t = time.Now()
	} else {
		t, err = Parse(format)
		if err != nil {
			return
		}
	}

	tu.times[uid] = t
	return nil
}

// Stop UIDに対する時間停止。UIDに対応した時間が既にないなら特に何もしない
func (tu *TimeUtil) Stop(uid string) {
	tu.cond.L.Lock()
	defer tu.cond.L.Unlock()
	_, ok := tu.times[uid]
	if !ok {
		return
	}
	delete(tu.times, uid)
}

// Now UIDに対する現在時刻(Startした時の値)を返す
func (tu *TimeUtil) Now(uid string) (time.Time, error) {
	t, ok := tu.times[uid]
	if !ok {
		return time.Time{}, errNotFound(uid)
	}
	return t, nil
}

// Format time.Time を Ymd 形式で返す
func Format(t time.Time) string {
	return t.Format(YmdLayout)
}

// Parse Ymd 形式から time.Time へパース
func Parse(ymd string) (time.Time, error) {
	return time.Parse(YmdLayout, ymd)
}
package main

import (
	"fmt"
	"testing"

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

func TestNow(t *testing.T) {
	t.Parallel()

	tests := []struct {
		expected string
	}{
		{"2020-02-02 11:22:33"},
		{"2020-02-12 11:22:33"},
	}
	for i, tt := range tests {
		t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) {
			t.Parallel()

			ctx, def := setUp(t, tt.expected)
			defer def()
			uid := GetUID(ctx)
			n, err := Time().Now(uid)
			assert.Nil(t, err)
			assert.Equal(t, tt.expected, Format(n))
		})
	}
}
package main

import (
	"context"
	"fmt"
	"testing"

	"github.com/google/uuid"
)

// TestMain テストの初期化
func TestMain(m *testing.M) {
	InitializeTimeUtil()

	code := m.Run()
	if code != 0 {
		panic(fmt.Errorf("test: exit %d", code))
	}
}

func setUp(t *testing.T, currentTime string) (context.Context, func()) {
	uid := uuid.New().String()
	ctx := SetUID(context.Background(), uid)

	tu := Time()
	if err := tu.Start(uid, currentTime); err != nil {
		t.FailNow()
	}

	return ctx, func() {
		tu.Stop(uid)
	}
}

実際, これを使ったサンプルはこんなん

package main

import (
	"context"
	"time"
)

// Todo entity
type Todo struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	ExpiredAt time.Time `json:"expired_at"`
}

// IsExpired 期限ぎれかチェック。期限切れ時刻が現在時刻を過ぎてたら true
func (t *Todo) IsExpired(ctx context.Context) (bool, error) {
	uid := GetUID(ctx)
	n, err := Time().Now(uid)
	if err != nil {
		return false, err
	}
	return (n.Unix() < t.ExpiredAt.Unix()), nil
}

普通に引数で time を回す分には注入をどこでやるかの層に限界があるし, time.Time を伝搬させていくのってなんか微妙 ..... ということで context に設定して取れるようにしてみた。

package main

import (
	"testing"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
)

func TestIsExpired(t *testing.T) {
	t.Parallel()

	currentTime := "2020-12-04 12:23:34"

	tests := []struct {
		title     string
		expected  bool
		expiredAt string
	}{
		{"現在時刻が期限を過ぎてるので期限切れ", true, "2020-12-02 12:23:34"},
		{"現在時刻が期限を過ぎていない", false, "2020-12-12 12:23:34"},
		{"現在時刻が期限と同一時刻", false, currentTime},
	}

	for _, tt := range tests {
		t.Run(tt.title, func(t *testing.T) {
			t.Parallel()

			ctx, def := setUp(t, currentTime)
			defer def()

			expiredAt, err := Parse(tt.expiredAt)
			assert.Nil(t, err)

			todo := &Todo{
				ID:        uuid.New().String(),
				Name:      "test todo",
				ExpiredAt: expiredAt,
			}
			actual, err := todo.IsExpired(ctx)
			assert.Nil(t, err)
			assert.Equal(t, tt.expected, actual)
		})
	}
}

テスト自体は同時実行安全になってて, t.Parallel でも問題なし。

あと嬉しいのが例えば E2E での時間ズラしながらの確認とかにも使える。便利 🙌