さて、Rubyでいろいろ開発していて時間(時刻・期間…)が絡むテストをするときTimecopが便利で広く使われます。
travisjeffery/timecop – GitHub
# 現在時刻を変える > Timecop.travel(Date.parse('1998/12/1')) => 1998-12-01 00:00:00 +0900 > Date.today => Tue, 01 Dec 1998 # 時間の進行を止める > Timecop.freeze(Time.parse('1998/12/1 0:00:00')) => 1998-12-01 00:00:00 +0900 > sleep(10) > Time.now => 1998-12-01 00:00:00 +0900 # it's still 0:00:00 # 時間の進行を早める > Timecop.travel(Time.parse('1998/12/1 0:00:00')) > Timecop.scale(3600) > sleep(2) > Time.now => 1998-12-01 02:00:19 +0900 # it took 2 hours although only slept for 2 seconds
便利ですね。
時間をいじくるので、
- タイムゾーン周りをミスってないか
- DBMSのタイムスタンプやredisのexpireのようにruby処理系の時間を使わないものでは効かない
- …
こんなかんじで注意が必要なのは当然なのですが、一番しんどいのはTimecopで変えた時間を戻し忘れたときの挙動不審なかんじでしょうか。
テストでTimecopを使う場合、このテのミスをするとたいてい別のテストケースに影響しますね。順序依存で効いてくるし煽りを食ったテストケース自体は何も悪く無いことがほとんどなので、心神耗弱状態になります。
- なぜかCapybaraのCookieが取れなくなっている
- redisではうまくいくのにredis_mockでテストすると想定外の挙動になる
- Timeoutが狂う
- …
こんなことで時間をムダにするのはあほらしいので、Timecop.returnを呼ぶのは絶対に忘れないようにしましょう。
> Timecop.travel(Time.parse('1998/12/1 0:00:00')) > Timecop.return > Date.today => Sun, 20 Dec 2015
「忘れないでね」「気をつけてね」はものごとの解決策としては最悪な思考停止なので、
Rails/RSpecをお使いなら、spec/spec_helper.rbにて
config.after(:each) do Timecop.return end
なる設定をしておくべきです。
また、別の安全策として、Timecopのsafe_modeといい、Timecopを使うときはblockを与えることを強制し、block末尾でreturnが自動で行われるようにする仕組みがあります。
blockを渡さずに呼ぶと例外が飛ぶので、すぐに気づきます。
> Timecop.safe_mode = true > Timecop.travel(Time.parse('1998/12/1 0:00:00')) Timecop::SafeModeException: Safe mode is enabled, only calls passing a block are allowed. ... > Timecop.travel(Time.parse('1998/12/1 0:00:00')) do > Date.today > end => Tue, 01 Dec 1998 > Date.today => Sun, 20 Dec 2015
Rails/RSpecのみで使うなら、spec_helperのafterでケアするのがシンプルにして安全十分かと。
テスト以外でも使うなら、safe_modeにするようinitializerなどで設定しておくほうが安全そうです。
全部Githubに書いてあるんですけども、1ヶ月近くこれにハマってしまった自分が本当にあほらしかったので忘れないように…。