5.5 時間注入與 deterministic test
時間注入的核心目標是讓測試可以控制「現在時間」。只要函式內部直接呼叫 time.Now(),測試結果就可能受執行時間影響;把時間來源改成參數或小介面後,測試就能重現固定情境。
真實時間會讓測試不穩定
測試不穩定的核心原因是輸入不完全由測試控制。時間是最常見的隱性輸入,因為 time.Now() 每次呼叫都會得到不同結果。
1func IsExpired(deadline time.Time) bool {
2 return time.Now().After(deadline)
3}這個函式看起來只有一個參數,但實際上還依賴目前時間。測試如果用 time.Now().Add(...) 組 deadline,可能因為執行延遲、時區或邊界條件而變得脆弱。
更好的做法是把現在時間傳進去。
1func IsExpired(now time.Time, deadline time.Time) bool {
2 return now.After(deadline)
3}函式的所有重要輸入都變成參數後,測試可以完全控制情境。
參數注入適合純邏輯
參數注入的核心用途是處理單次計算。當函式只是判斷過期、計算剩餘時間或產生 timestamp,直接把 now time.Time 傳入通常最簡單。
1func Remaining(now time.Time, deadline time.Time) time.Duration {
2 if now.After(deadline) {
3 return 0
4 }
5 return deadline.Sub(now)
6}測試可以建立固定時間點。
1func TestRemaining(t *testing.T) {
2 now := time.Date(2026, 4, 22, 10, 0, 0, 0, time.UTC)
3 deadline := time.Date(2026, 4, 22, 10, 5, 0, 0, time.UTC)
4
5 got := Remaining(now, deadline)
6 want := 5 * time.Minute
7
8 if got != want {
9 t.Fatalf("Remaining() = %v, want %v", got, want)
10 }
11}這個測試不會因為今天是哪一天、測試跑得快或慢而改變結果。參數注入也讓函式更容易理解,因為時間依賴直接出現在函式簽名中。
provider 函式適合需要多次取時間的元件
時間 provider 的核心用途是讓長生命週期元件可以取得目前時間,但測試仍能替換時間來源。最簡單的 provider 是 func() time.Time。
1type TokenGenerator struct {
2 now func() time.Time
3}
4
5func NewTokenGenerator(now func() time.Time) TokenGenerator {
6 return TokenGenerator{now: now}
7}
8
9func (g TokenGenerator) NewToken(userID string) Token {
10 return Token{
11 UserID: userID,
12 CreatedAt: g.now(),
13 }
14}正式環境可以傳入 time.Now。
1generator := NewTokenGenerator(time.Now)測試可以傳入固定時間。
1func TestTokenGenerator(t *testing.T) {
2 fixedNow := time.Date(2026, 4, 22, 10, 0, 0, 0, time.UTC)
3 generator := NewTokenGenerator(func() time.Time {
4 return fixedNow
5 })
6
7 token := generator.NewToken("user_123")
8 if !token.CreatedAt.Equal(fixedNow) {
9 t.Fatalf("CreatedAt = %v, want %v", token.CreatedAt, fixedNow)
10 }
11}func() time.Time 比完整介面更輕量,適合只需要目前時間的情境。若元件還需要 timer、ticker 或 sleep,才需要更完整的 clock abstraction。
duration 測試應控制時間
測試 timeout 的核心原則是驗證邏輯,不是讓測試真的睡很久。time.Sleep 會讓測試慢,也會讓測試受排程影響。
1func RetryDelay(attempt int) time.Duration {
2 if attempt <= 0 {
3 return 0
4 }
5
6 return time.Duration(attempt) * 100 * time.Millisecond
7}這種邏輯應該直接測回傳的 duration。
1func TestRetryDelay(t *testing.T) {
2 got := RetryDelay(3)
3 want := 300 * time.Millisecond
4
5 if got != want {
6 t.Fatalf("RetryDelay() = %v, want %v", got, want)
7 }
8}若真的要測等待行為,應把等待機制包成可替換依賴,讓測試使用 fake sleeper,而不是呼叫真實 time.Sleep。
1type Sleeper interface {
2 Sleep(time.Duration)
3}
4
5type realSleeper struct{}
6
7func (realSleeper) Sleep(d time.Duration) {
8 time.Sleep(d)
9}這種抽象只有在等待行為本身需要測試時才值得加入。不要為了形式而把所有 time API 都包起來。
時區要明確
時間測試的核心規則是使用明確時區。測試資料若依賴本機時區,可能在不同開發機或 CI 環境得到不同結果。
1createdAt := time.Date(2026, 4, 22, 10, 0, 0, 0, time.UTC)使用 time.UTC 能讓測試在不同環境保持一致。若功能本來就和特定時區有關,應用 time.LoadLocation 明確載入。
1loc, err := time.LoadLocation("Asia/Taipei")
2if err != nil {
3 t.Fatalf("load location: %v", err)
4}
5
6localTime := time.Date(2026, 4, 22, 18, 0, 0, 0, loc)不要讓測試默默依賴 time.Local,除非測試目的就是驗證本機時區設定。
下一章
下一章會進入並發行為測試,說明如何驗證 goroutine、channel 與共享狀態。
延伸閱讀
本章處理入門測試中的時間依賴。若要測長時間 worker、ticker 排程、WebSocket cleanup 或 deadline,可以接著閱讀 Go 進階:時間注入與狀態轉移測試;若 timeout 來自部署平台或 load balancer,則閱讀 Go 進階:Kubernetes、systemd 與 load balancer 合約。