はじめに
テストは本来、コードの品質を守る砦です。しかし「日付」を扱うテストは、思わぬ落とし穴を含んでいます。
ある日突然テストが赤くなる。
昨日まで通っていたテストが、翌朝エラーになる。
未来の日付をテストに書いていた箇所が、気づいたら「過去の出来事」になっている。
こうした 日付を原因として将来テストが壊れる現象 を、開発者の間では俗に 「タイムボム」 と呼びます。
この記事では、
- タイムボムとは何か
- なぜ日付関連テストは壊れやすいのか
- Golangでタイムボムを消すベストプラクティス
をまとめてみました
タイムボムとは
タイムボム(Time Bomb) とは、
「テストコード内に特定の日付や時間が書き込まれており、その日時を過ぎるとテストが自然に壊れてしまう問題」
を指します。
func TestBirthdayDiscount(t *testing.T) {
today := time.Now()
user := User{Birthday: time.Date(2025, 12, 25, 0, 0, 0, 0, time.UTC)}
if !user.HasBirthday(today) {
t.Fatal("should be birthday")
}
}
このテストは、12月25日に走ると成功します。
しかし 翌日には必ず落ちます。
これは 固定日付がビジネスロジックに組み込まれているにも関わらず、テスト側が明示的に固定日付を与えていないためです。
なぜ日付を使うテストは壊れやすい?
1. time.Now() が可変である
time.Now() は環境依存で、テスト実行日時によって値が変わるため、
同じコードでも毎日結果が変わる可能性があります。
2. タイムゾーン違いによるズレ
CI とローカル環境でタイムゾーンが異なり、時差でテストが落ちることがあります。
3. 年度・月初・月末に敏感
例えば「翌月1日を計算するテスト」などは、月末に近づくと落ちやすいです。
特に閏年は1年で最も単体テストが落ちる日ではないでしょうか・・・😭
ベストプラクティス①:依存性として “時計” を注入する
もっともよく使われるのがこのパターン。
clock interface を定義する
type Clock interface {
Now() time.Time
}
type RealClock struct{}
func (RealClock) Now() time.Time { return time.Now() }
テストでは固定時刻を返す FakeClock を用意
type FakeClock struct {
Fixed time.Time
}
func (f FakeClock) Now() time.Time {
return f.Fixed
}
使用側:time.Now() を使わず clock.Now() に統一
type Service struct {
Clock Clock
}
func (s Service) IsTodayBirthday(birthday time.Time) bool {
today := s.Clock.Now()
return today.Month() == birthday.Month() && today.Day() == birthday.Day()
}
テスト
func TestIsTodayBirthday(t *testing.T) {
fixed := time.Date(2025, 2, 20, 0, 0, 0, 0, time.UTC)
s := Service{
Clock: FakeClock{Fixed: fixed},
}
birthday := time.Date(1990, 2, 20, 0, 0, 0, 0, time.UTC)
if !s.IsTodayBirthday(birthday) {
t.Fatal("expected true")
}
}
→ 固定日時でテストできるため永遠に壊れない。
ベストプラクティス②:テストの固定日時は常にコードに書く(Now を基準にしない)
悪い例:
target := time.Now().AddDate(0, 0, 1) // 明日の予約
良い例:
fixed := time.Date(2025, 2, 10, 12, 0, 0, 0, time.UTC)
target := fixed.AddDate(0, 0, 1)
「time.Now を基準にするテスト」は不正確になることが多いです。
ベストプラクティス③:テストデータの日付は “相対的意味” を持つようにする
これは実務で非常に役立ちます。
例:
「30日以内なら有効」というロジックのテスト
悪い:
expiration := time.Date(2024, 12, 1, 0, 0, 0, 0, time.UTC) // そのうち過去になる
良い:
base := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) // 固定
expiration := base.AddDate(0, 0, 30)
→ ベースを固定日時にし、相対的な指定であれば「30日以内」が常に維持される。
おわりに
最後まで読んでくださりありがとうございます!
日付の扱いは、テストコードを壊す原因のトップ候補に上がってきます。
特に昨日まで動いていたテストが何も弄ってないのに突然落ちるケースは十中八九タイムボム系かと思います。。。😭
time.Now()をむやみやたらに使わず、固定日時にするだけでかなり信頼性が向上するかと思います!
次の閏年が楽しみです…😏