テスト書く時とかに便利
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 での時間ズラしながらの確認とかにも使える。便利 🙌