Rails

[RSpec]日付が関連するテストはtravel toを使って日付を固定する

はじめに

日付に関するテストは必要になることが多いと思います。

たとえば年末年始や夏季休業のときだけ, 「〇〇月〇〇〜〇〇月〇〇日までお休みします」や, 「8月〇〇〜〇〇日はサマーセール実施中!」というメッセージを表示したいなど。

こういった日時によって結果が変わる実装のテストを作るには日付を固定するヘルパーメソッドを使うといいです。
日付を固定するメソッドはtravel_toです。

Railsのヘルパーメソッドを使うだけなので導入する手間は大してかかりません。
ActiveSupport::Testing::TimeHelpers

もし気になる方がいたらご一読ください〜

想定される読者

  • Ruby・Railsを勉強し始めたばかりの人
  • 基本は勉強したけど、まだRailsでテストを書いたことがない人

現役でバリバリ書いてる人には物足りない記事になると思います。

僕自身エンジニアを目指して頑張っている状態なので、間違っている点などがありましたら教えてもらいたいです。

参考記事

ActiveSupport::Testing::TimeHelpers

結論

さっさとコードだけ見せてくれ、って思う人が多いと思うのでサンプルコードを載せておきまーす。

実行環境

  • Ruby 3.1.1p18
  • Rails 6.1.5
  • rails c上で実行
  • RailsのタイムゾーンはTokyoに設定(タイムゾーンの設定をしていないとNoMethodErrarになるので注意)

サンプルコードはこちら

# 実行環境のTimeゾーンの設定はTokyoです
Time.zone.name
=> Tokyo

# Time.zoneで現在時刻表示する
Time.zone.now
=> Fri, 05 Aug 2022 10:37:22.640227000 JST +09:00

# travel toを使って現在時刻を設定・固定化する
travel_to Time.zone.local(2022, 7, 30)

# 時刻が固定化されてるか確認する
Time.zone.now
=> Sat, 30 Jul 2022 00:00:00.000000000 JST +09:00

いつ使うのか

  • はじめにでも書いたように、時期によって変更したいものがある場合に使える
  • たとえば以下のような実装が必要になったと想定する。
  • 8月4日から8月11日までは「全品半額セール実施中!」と表示する
  • 上の日程以外は「毎日、元気に営業中!」と表示する
special_message.rb

# frozen_string_literal: true

class SpecialMessage
  # 日付によって表示するメッセージを変更するメソッドを作成
  def sale_information(time)
    if time.month == 8 && time.day >= 4 && time.day <= 11
      "全品半額セール実施中!"
    else
      "毎日、元気に営業中!"
    end
  end
end


ではこのメソッドを実行していこう。

# irbで実行

# 日付を取得する
time = Time.zone.now
=> Fri, 05 Aug 2022 11:17:02.127135000 JST +09:00

# メソッドを実行する
special_message = SpecialMessage.new special_message.sale_information(time)
=> "全品半額セール実施中!"

 

このようなメソッドに対するテストを作成するときにtravel toが必要になります。

テストを書いてみる

では上で書いたsale_inforation(time)のテストを書いてみましょう。

sale_inforation(time)のテストを書く

えーっと、まずは現在の日付と時刻を取得しよう。

その次に、今日の日付にあった文字列が返されればいいかな。

ではテストを書いていく。

spec/sale_message_spec.rb

# frozen_string_literal: true

require &#039;rails_helper&#039;

RSpec.describe SaleMessage, type: :model do
  it '今日が8月4~11日だったらセール情報を返す' do
    time = Time.zone.now
    special_message = SpecialMessage.new
    expect(special_message.sale_information(time)).to eq '全品半額セール実施中!"
    expect(special_message.sale_information(time)).to_not eq '毎日、元気に営業中!'
  end
end


さっそくテストを実行してみると、

$ bundle exec rspec spec/sale_message_spec.rb

SaleMessage
  今日が8月4~11日だったらセール情報を返す

Finished in 0.01626 seconds (files took 1.47 seconds to load)
1 example, 0 failures


テストが通った!

このテストの問題点

ただしこのテストには問題があります。

まず第一に通常営業時に表示されるテストがありません。

「じゃあテストを作ればいいじゃないか!と思いますよね。

でも本日の日付(8月5日)がセール期間中なのでTime.zone.nowを使って日付を取得している限り、今日何回テストをやっても通常営業のメッセージを表示するテストはパスしません。

それどころか8月12日以降は[今日が8月4~11日だったらセール情報を返す]テストも落ちるようになってしまいます。

この問題を解決するにはtravel toというメソッドが必要になります。

travel toを使ったテストを書いてみる

先ほど作成したテストの問題点は日付が変動することに対応できていなところです。

なのでtravel toを使って日付を変動させなければ問題は解決できます。

travel toの使い方は簡単です。

以下のコマンドを打ち込めば日付が固定されます。

# xxには自分が設定したい西暦、月、日付を入力する
travel_to Time.zone.local(xx, xx, xx)

では試してみましょう。

# 実行環境のTimeゾーンの設定はTokyoです
Time.zone.name
=> Tokyo

# Time.zoneで現在時刻表示する
Time.zone.now
=> Fri, 05 Aug 2022 10:37:22.640227000 JST +09:00

# travel toを使って現在時刻を設定・固定化する
travel_to Time.zone.local(2022, 7, 30)

# 時刻が固定化されてるか確認する
Time.zone.now
=> Sat, 30 Jul 2022 00:00:00.000000000 JST +09:00

travel toを使ったあとは指定した日付に変わっていますよね。

それでは次にtravel toを使って、SaleMessageのテストを修正していきます。

# frozen_string_literal: true
require &#039;rails_helper&#039

RSpec.describe SaleMessage, type: :model do 
 it '今日が8月4~11日だったらセール情報を返す' do 
   travel_to Time.zone.local(2022, 8, 10)
   time = Time.zone.now
   special_message = SpecialMessage.new
   expect(special_message.sale_information(time)).to eq '全品半額セール実施中!'
   expect(special_message.sale_information(time)).to_not eq '毎日、元気に営業中'
 end
 it '今日が8月3日以前だったら通常営業メッセージを返す' do
   travel_to Time.zone.local(2022, 8, 3)
   time = Time.zone.now
   special_message = SpecialMessage.new
   expect(special_message.sale_information(time)).to_not eq '全品半額セール実施中'
   expect(special_message.sale_information(time)).to eq '毎日、元気に営業中!'
 end
 it '今日が8月12日以降だったら通常営業メッセージを返す' do
   travel_to Time.zone.local(2022, 8, 12)
   time = Time.zone.now
   special_message = SpecialMessage.new
   expect(special_message.sale_information(time)).to_not eq '全品半額セール実施中'
   expect(special_message.sale_information(time)).to eq '毎日、元気に営業中!'
 end
end

* わかりやすさを大事にしたいので、あえてコードはDRYにしていません。お気を悪くされたらすみません

テストは全部パスしましたー!

おわりに

今回はtravel toの使い方についてまとめました。
わかりにくい箇所や間違っていることがあったら教えてください🙏