Rubyの時間を止めるには


2023年 11月 02日

こんにちは。SI部の r_maeda です。

Webアプリケーションを作っていると、現在時刻に依存したコードを書くことがよくあります。(e.g. 「期限が設定された課題」があり、「期限が迫っている」場合は表示を目立たせたい、など。)

そんな「時間に依存したコード」をテストする場合、

  • テストコードが動いている間は時間を止めたい
  • ちょうど「任意の時間」が経過したタイミングでテストを実行したい

といったことを考えると思います。

このような場面において、Rubyコードのテストでは、以下の記事でも紹介されているとおり

  • timecop (Timecop モジュール)
  • activesupport (ActiveSupport::Testing::TimeHelpers モジュール)

あたりの gem がよく用いられています。

今回の記事では、そんな TimecopActiveSupport::Testing::TimeHelpers にはどんな違いがあるのか、これをもう少し掘り下げて比較してみようと思います。

時間を止める/任意の日時に移動する

Timecop では「任意の日時に移動しつつ、時間は止めない」ことができますが、 ActiveSupport::Testing::TimeHelpers では「移動すると同時に時間を止める」ことしかできません。

## Timecop
Timecop.freeze do
  # 現在時刻で時間を止める
end

Timecop.travel(time_or_date) do
  # time_or_date に渡された時間に移動するが、時間は止まらない
end

Timecop.freeze(time_or_date) do
  # time_or_date に渡された時間に移動し、時間が止まる
end

## ActiveSupport::Testing::TimeHelpers
ActiveSupport::Testing::TimeHelpers.travel_to(date_or_time) do
  # time_or_date に渡された時間に移動し、時間が止まる
  # **このとき、date_or_time.to_time.usec は強制的に 0 に設定されます**
end

ActiveSupport::Testing::TimeHelpers.freeze_time do
  # 現在時刻で時間を止める
  # = travel_to(Time.now) と同じ
end

# NOTE: Rails 7.1 からは、秒未満の時間を保持したまま旅行するためのオプションが追加されました
ActiveSupport::Testing::TimeHelpers.freeze_time(with_usec: true) do
  ...
end

時間の流れを変える

Timecop では、「1秒あたりの経過時間」を変更することが可能です。これは ActiveSupport::Testing::TimeHelpers ではサポートされていません。

## https://github.com/travisjeffery/timecop/tree/master#timecopscale より引用

# seconds will now seem like hours
Timecop.scale(3600)
Time.now
# => 2012-09-20 21:23:25 -0500
# seconds later, hours have passed and it's gone from 9pm at night to 6am in the morning
Time.now
# => 2012-09-21 06:22:59 -0500

ブロックを与えずに時間を操作することを禁止する

これも Timecop のみですが、freeze travel scale のそれぞれのメソッドが、ブロックを与えられずに呼び出された場合に、例外を raise するようになります。

ブロックなしでのこれらのメソッドの呼び出しを禁止することで、Timecop.return の呼び忘れを防ぐことができます。

## https://github.com/travisjeffery/timecop/tree/master#timecopsafe_mode より引用

# turn on safe mode
Timecop.safe_mode = true

# check if you are in safe mode
Timecop.safe_mode?
# => true

# using method without block
Timecop.freeze
# => Timecop::SafeModeException: Safe mode is enabled, only calls passing a block are allowed.

まとめ

今回の記事では、時間に依存したRubyコードをテストするために良く使われる TimecopActiveSupport::Testing::TimeHelpers の 2 つのモジュールを取り上げ、その機能の違いを比較してみました。

比較してみたところ、Timecop モジュールのほうが多機能であることがわかりましたが、Ruby on Rails を使っている方にとっては ActiveSupport::Testing::TimeHelpers モジュールのほうが導入が容易であり、
また個人的には「多くのテストケースにおいては ActiveSupport::Testing::TimeHelpers モジュールでも必要十分である」とも考えています。

この記事を書き始めたきっかけは

  • 既存のテストコードで利用していた Timecop.freezeActiveSupport::Testing::TimeHelpers.freeze_time に置き換えた結果、テストが失敗するようになった
  • その原因を調べていった結果、秒未満の時間が切り捨てられる違いがあった

といったものからでしたが、「秒未満の時間が勝手に切り捨てられる」挙動も、考え方を変えれば「テストを書くにあたって、秒未満の時間を気にする必要がなくなる」とも言い換えられるでしょう。

これら 2つのモジュールの違いを理解した上で、より「自分のテストコードに合っているものはどちらなのか」を考えて使いわけられるようになると良いですね。