RSpec

【RSpec】現在時刻(Time.current, Time.zone.now)をテストする3つの方法

RSpec

 

今回はRSpecで現在日時(Time.currentやTime.zone.now)をテストする3つの方法についてまとめてみました。コード内で現在日時を使用した場合、そのテストコードでは現在日時部分が固定されていないため少し工夫する必要があります。今回はその解決方法を3つ紹介したいと思います。

RSpecでは正確な日時は特定できない

以下のようにコード内で現在日時を使用している場合、RSpecではTime.currentで日時を設定するタイミングが異なるため正確な日時を特定することはできません。

# posts_controller.rb

def update
  @post = Post.find(params[:id])
  @post.update!(posted_at: Time.current) ← 投稿日時の更新を行う
  redirect_to posts_path
end
describe "PATCH /posts/:id" do
  let(:post) { create(:post) }

  it '投稿日の更新が行われる' do
    patch post_path(post)
    expect(post.reload.posted_at).to eq Time.current
  end
end
Failure/Error: expect(post.posted_at).to eq Time.zone.now
     
  expected: 2021-10-07 12:21:00.563344000 +0900
       got: 2021-10-07 12:21:09.558092000 +0900

現在時刻をテストする3つの方法

テスト実行前の現在日時と評価時の現在時刻を用いる

1つ目の方法はテスト実行前に現在日時を保存し、その日時が評価時の現在時刻との間にあるかをチェックするようにします。今回はテスト実行前の現在日時をstart_timeとして保存しています。

describe "PATCH /posts/:id" do
  let(:post) { create(:post) }
  let!(:start_time) { Time.current }

  it '投稿日時が更新される' do
    patch post_path(post)
    expect(post.reload.posted_at).to be_between(start_time, Time.current)
  end
end

インスタンスの値を更新するためreloadが必要です。

expect(post.reload.posted_at)

travel_toを用いて現在日時を固定する

Rails4.1以降、ActiveSupportにtravel_toメソッドが実装され、それを用いることで現在日時を固定しテストすることができます。

Changes current time to the given time by stubbing Time.now, Date.today, and DateTime.now to return the time or date passed into this method. The stubs are automatically removed at the end of the test.

Time.now、Date.today、およびDateTime.nowをスタブして、このメソッドに渡された時刻または日付を返すことにより、現在の時刻を指定された時刻に変更します。スタブは、テストの終了時に自動的に削除されます。

参考: ActiveSupport::Testing::TimeHelpers

 

travel_toメソッドを使用するにはrails_helper.rbにincludeする必要があるため、まずは以下の記述を忘れずに追記しましょう。

RSpec.configure do |config|
  # 省略

  # 以下の1行を追記
  config.include ActiveSupport::Testing::TimeHelpers

  # 省略
end

 

travel_toメソッドを用いて投稿日時が更新されるテストは、以下のように記述することができます。aroundでtravel_toを囲んでおくことで、他のitでも現在日時が固定されるので便利です。

describe "PATCH /posts/:id" do
  let(:post) { create(:post) }

  around { |e| travel_to(Time.current) { e.run } }

  it '投稿日時が更新される(travel_toを用いて現在日時を固定する方法)' do
    patch post_path(post)
    expect(post.reload.posted_at).to eq Time.current
  end

  it '投稿日時が更新される(aroundの挙動テスト)' do
    patch post_path(post)
    expect(post.reload.posted_at).not_to eq '2021-12-31' 
  end
end

 

以下のようにbeforeブロックで囲んで使用することもできます。

describe "PATCH /posts/:id" do
  let(:post) { create(:post) }

  before do
    travel_to(Time.current)
  end

  it '投稿日時が更新される(travel_toを用いて現在日時を固定する方法)' do
    patch post_path(post)
    expect(post.reload.posted_at).to eq Time.current
  end
end

Rails5.2系以前はtravel_toで日時を固定した場合、以下のように必ずtravel_backで時間を戻しておく必要がありました。

describe "PATCH /posts/:id" do
  let(:post) { create(:post) }

  before do
    travel_to(Time.current)
  end

  after do
    travel_back
  end

  it '投稿日時が更新される(travel_toを用いて現在日時を固定する方法)' do
    patch post_path(post)
    expect(post.reload.posted_at).to eq Time.current
  end
end

 

しかし5.2系にてこれを修正するPRが出されマージされているため、travel_backは不要になりました。

activesupport/lib/active_support/testing/time_helpers.rbの修正です。

TimeHelpers moduleでafter_teardownメソッドを定義し、テスト終了時に自動でtravel_backを呼び出すよう修正しています。

これにより、block無しのtravelやtravel_toを使用した場合に、明示的にtravel_backを呼ぶ必要は無くなります。 Remove automatic removal of Date/Time stubs after each test caseで一度同様の対応が削除された事があったのですが、自動でtravel_backした方が便利だろう、という事で再度入ったようです。

参考: rails commit log流し読み(2017/07/24)

Timecopを用いて現在日時を固定する

travel_toではなく、Timecopというgemを用いることでも現在日時を固定することが可能です。まずは以下の手順でrspec-timecopを導入しましょう。

gem 'rspec-timecop'
% bundle install

 

これでTimecop.freezeで日時を固定することができるようになりました。

describe "PATCH /posts/:id" do
  let(:post) { create(:post) }
    
  around { |e| Timecop.freeze(Time.current) { e.run } }

  it '投稿日時が更新される(Timecopを用いて現在日時を固定する方法)' do
    patch post_path(post)
    expect(post.reload.posted_at).to eq Time.current
  end
end

 

travel_toの場合と同じように、beforeブロックで囲んで使用することもできます。

describe "PATCH /posts/:id" do
  let(:post) { create(:post) }

  before do
    Timecop.freeze(Time.current)
  end

  after do
    Timecop.return
  end

  it '投稿日時が更新される(Timecopを用いて現在日時を固定する方法)' do
    patch post_path(post)
    expect(post.reload.posted_at).to eq Time.current
  end
end

Timecopで時間を固定した際は、Timecop.returnで時間を戻しておく必要があります。上記コードではafterブロックで使用していますが、以下のようにspec_helper.rbに設定を追記しておいても良いかもしれません。

config.after(:each) do
  Timecop.return
end

まとめ

  • 現在日時を設定するタイミングが異なるため、テストコード内では正確な日時を特定することはできない
  • 1つ目の方法は、テスト実行前に現在日時を保存し、その日時が評価時の現在時刻との間にあるかをチェックする
  • 2つ目の方法は、travel_toメソッドを用いて現在日時を固定する
  • 3つ目の方法は、Timecopを用いて現在日時を固定する
  • Timecopを使用した際は、Timecop.returnで時間を戻しておく必要がある

参考

 

 

今回はRSpecで現在日時をテストする3つの方法について紹介しました。個人的にはtravel_toを用いて現在日時をテストする方が最もシンプルで正確なのかなと思います。