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.now
と Date.today
と DateTime.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の便利なメソッドは使い方を見ただけで簡単に使えることが多いですが、改めてていねいに実装を読んでみました。
読んでみると意外に難しい処理は行われていないことがわかりました。
新しいメソッドを使うときは、このように本体の実装に立ち返って理解するようにしたいと思います。