Railsのtravel_toの実装を読む

· 1088 words · 3 minute read

travel_toを使ったテスト

もうすぐ今年も終わりますね。

年末年始は会社も休業するので、仕事で関わっているサービスでも休業期間のご案内を表示しています。

"年末年始休業のお知らせ 12月26日(水)~翌1月3日(木)は年末年始休業となります。"

この「一定期間だけ表示する文言」のテストを書くときに、travel_toというRailsのメソッドを使うといいというアドバイスをもらいました。


  context '表示期間内の場合' do
    it '休業情報を表示すること' do
      travel_to Time.zone.local(2018, 12, 26) do
        # Time.current => Wed, 26 Dec 2018 00:00:00 JST +09:00
        visit path
        expect(page).to have_content '年末年始休業のご案内'
      end
    end

こんな風に書くことで、テストの中のTime.current の値をブロック内で擬似的に Wed, 26 Dec 2018 00:00:00 JST +09:00 として扱うことができます。

初めてみたときはRailsらしい魔法のような簡潔で便利なメソッドだと思いました。

travel_toの実装を読む

どうやってこのような挙動をしているのか、Rails本体の実装を見てみます。

api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers

ActiveSupportのTestingのTimeHelpers内で実装されていますね。

値をスタブ(テスト用の代用品)にして Time.nowDate.todayDateTime.nowの形で返すことができるそうです。


# File activesupport/lib/active_support/testing/time_helpers.rb, line 113

def travel_to(date_or_time)
  if block_given? && simple_stubs.stubbing(Time, :now)
    travel_to_nested_block_call = <<-MSG.strip_heredoc

Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing.

Instead of:

   travel_to 2.days.from_now do
     # 2 days from today
     travel_to 3.days.from_now do
       # 5 days from today
     end
   end

preferred way to achieve above is:

   travel 2.days do
     # 2 days from today
   end

   travel 5.days do
     # 5 days from today
   end

    MSG
    raise travel_to_nested_block_call
  end

  if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime)
    now = date_or_time.midnight.to_time
  else
    now = date_or_time.to_time.change(usec: 0)
  end

  simple_stubs.stub_object(Time, :now) { at(now.to_i) }
  simple_stubs.stub_object(Date, :today) { jd(now.to_date.jd) }
  simple_stubs.stub_object(DateTime, :now) { jd(now.to_date.jd, now.hour, now.min, now.sec, Rational(now.utc_offset, 86400)) }

  if block_given?
    begin
      yield
    ensure
      travel_back
    end
  end
end

処理の流れ

1. 引数をTimeクラスに変換する
引数が Date型(YYYY-MM-DD)で渡されたときは時刻を00:00:00にして与えてあげて、
Datetime型(YYYY-MM-DD HH:MM:SS)で渡されたときはそのまま、
Timeクラス(YYYY-MM-DD HH:MM:SS +0900)に変換してあげています。

2. スタブに入れる
そして Time.now, Date.today, DateTime.nowのスタブに入れてあげています。

3. 元に戻す
最後に ensureでブロックを出るときに必ずtravel_back(travel_toで擬似的に入れた値を、現在日時に戻すメソッド)するようにしています。


おわりに

Railsの便利なメソッドは使い方を見ただけで簡単に使えることが多いですが、改めてていねいに実装を読んでみました。

読んでみると意外に難しい処理は行われていないことがわかりました。

新しいメソッドを使うときは、このように本体の実装に立ち返って理解するようにしたいと思います。