【Rails】【RSpec】書いた翌日に壊れるテスト、あなたは大丈夫?
はじめに
2025年4月1日。
普段通りに CI を回したところ、なぜかテストが突然落ちました。コードに変更は入っていないのに、です。
調査の結果、「日付を固定していなかった」ことが原因でした。
これは RSpec を書き始めたばかりの方が、つい見落としてしまいがちな落とし穴です。
この記事では、日付を扱うテストの落とし穴 と、その対策方法 について解説します。
テストが突然失敗するようになった
ある日落ちたテスト。該当の実装コードは以下のようなものでした。
実装側のコード(キャンセル料計算の例)
if self.created_at < Time.zone.local(2025, 4, 1)
calculate_cancel_fee_before_april # 2日前からキャンセル料発生するロジック
else
calculate_cancel_fee # 10日前からキャンセル料が発生するロジック
end
キャンセル処理の仕様変更があり、2025年4月1日
を境にロジックを切り替える必要があったため、このような分岐が入っていました。
そして、該当のテストは以下のように書かれていました。
テストコード
describe "キャンセル料計算" do
subject { order.cancel_fee }
let!(:order) { create(:order, date: 5.days.since.to_date) }
before { order.cancel! }
it "撮影5日前はキャンセル料がかからない" do
expect(subject).to eq 0
end
end
travel_to
もしていなければ、created_at
も明示的に指定していません。
この場合、order.created_at
はテスト実行時の現在日時になります。
つまり、2025年3月31日にテストを書いたときには created_at < 2025/4/1
だったため if
側が実行され、テストは通っていました。
しかし、4月1日を過ぎると created_at
がその日以降になってしまい、else
側が実行されるようになった のです。
結果としてテストが失敗しました。
なぜこの問題が起きたのか?
このような問題が起きる原因は、テストが、実行される「現在時刻」に依存しているためです。
Ruby(Rails)では、create(:order)
のようなファクトリーで生成されたモデルの created_at
は通常、テストを実行した時刻になります。
つまり、日付を明示的に指定しない限り、時間が経てばテストの実行結果が変わる可能性があるということです。
このようなテストは「時間依存のテスト」と呼ばれ、将来的に壊れる可能性があるテストになります。
travel_to
を使って時を止める
解決策:この問題を解決するには、テスト実行時の日付や時刻を固定する必要があります。
Rails には ActiveSupport::Testing::TimeHelpers
という便利なモジュールがあり、これを使うことで擬似的に「時を止める」ことができます。
修正後のテストコード例
include ActiveSupport::Testing::TimeHelpers
・・・
describe "キャンセル料計算" do
subject { order.cancel_fee }
before { travel_to("2025-3-31 10:00".in_time_zone) }
let!(:order) { create(:order, date: Date.new(2025, 4, 5)) }
before { order.cancel! }
it "5日前はキャンセル料がかからない" do
expect(subject).to eq 0
end
end
このように travel_to
を使えば、2025年3月31日の状態でテストを実行することができます。
さらに一歩踏み込んで:日付を明示的に指定する
また、可能であれば created_at:
などの属性を明示的に指定することで、よりテストの意図が明確になります。
let!(:order) { create(:order, created_at: Time.zone.local(2025, 3, 31)) }
このように書いておくと、テストがなぜそのロジックを通るのかが読み手にも伝わりやすくなります。
また、2025年4月1日で条件が切り替わるのであれば、2025年4月1日を指定したテストケース(境界値テスト)も作成したほうがいいですね。
まとめ:テストから変数を極限まで減らす
今回のように「日付によって動作が変わる」仕様を扱う際には、テストで以下の点に注意しましょう。
-
Date.today
やTime.zone.now
に依存しない -
travel_to
を使って日付・時間を固定する - 意図が読みやすいように、日付は明示的に指定する
- 書いたときに通るだけではなく、将来も通るか? を意識する
テストが未来によって壊されないように、「時間」をコントロールして、安定したテストを書いていきましょう 💪
おまけ:時間依存を検知する簡易チェックリスト
-
Date.today
やTime.zone.now
を使っていないか? -
created_at
やupdated_at
がロジックに影響していないか? - 過去・未来の日付をまたいで処理が変わるロジックがあるか?
もし当てはまるなら、一度テストを見直してみることをおすすめします。
Discussion