Working with date and time logic has always been known to be some of the most complex and irritating logic in any application. When you are writing a test case for codes which involve time sensitive functionality, you often encounter the need to create multiple test objects with different date and time attributes in order to cover both ‘happy path’ and ‘unhappy path’.
Let’s look at the simple example below, given the Item
class below, in order to test the expired?
method, you will need to create two objects, i.e., non_expired_item
and expired_item
to test both the happy and unhappy path.
# app/models/Item.rb
class Item < ApplicationRecord
def expired?
return true if expiration_date < Time.current
end
end
# spec/models/item_spec.rb
let!(:non_expired_item) { Item.create(:item, name: "Milk", expiration_date:
Time.current + 3.days) }
let!(:expired_item) { Item.create(:item, name: "Milk", expiration_date:
3.days.ago) }
described "#expired?" do
it "return false if item is not expired and true if item is expired" do
expect(non_expired_item.expired?).to eq(false)
expect(expired_item.expired?).to eq(true)
end
end
As you can see, the problem with the above approach is that you need to create a test object with a different expiration_date
for every different scenario that you want to test, causing your test to become slower and slower over time when you have more tests involving time related logic.
Fortunately, the Rails community has created Timecop gem that helps to “freeze time” and “time travel” during your tests, so that your time sensitive tests will not be affected when time elapses.
However, given that Rails 4.1 has introduced the TimeHelpers, which basically offers the same functionality as Timecop gem, there’s no reason to use timecop as we’ll be adding a dependency for functionality that’s already been provided by the Rails framework.
In this article, we will go through how to use ActiveSupport::Testing::TimeHelpers in testing with date and time logic.
Firstly, in order to use the TimeHelpers you have to include them into your tests
# spec/spec_helper.rb
RSpec.configure do |config|
configRSpec.configure do |config|
config.include ActiveSupport::Testing::TimeHelpers
end
Next, we are going to explore how to use the methods available in TimeHelpers such as travel
, travel_to
, travel_back
and freeze_time
.
Given the Item class below, I’ll illustrate how we can test the expired? method with TimeHelpers methods
class Item < ApplicationRecord
def expired?
return true if expiration_date < Time.current
end
end
1) travel(time_difference)
Time travel to the future given the time_difference
between current time and the future time.
let!(:item) { Item.create(:item, name: "Milk", expiration_date:
Time.current + 3.days) }
described "#expired?" do
it "return false if item is not expired and true if item is expired" do
expect(item.expired?).to eq(false) travel 5.day expect(item.expired?).to eq(true)
end
end
In the example above, the item is initially an non-expired item as the expiration_date is 3 days from current time. By calling travel 5.day
, we are basically forwarding the current time to 5 days from now, in which the item already expired.
2) travel_to(date_or_time)
Unlike travel, this method allows you to time travel to both future and past by specifying the date_or_time
.
let!(:item) { Item.create(:item, name: "Bean", expiration_date: Time.current) }
described "#expired?" do
it "return false when item is yet to be expired" do
travel_to(Time.current - 5.day) do
expect(item.expired?).to eq(false)
end
end
it "return true when item is expired" do
travel_to(Time.current + 5.day) do
expect(item.expired?).to eq(true)
end
end
end
In the first test above, you are traveling to the past when the item is yet to expire. While in the second test, you are traveling to the future, when the item already expired.
Note: You can also use a specific date and time like example below:-
Time.current # => Sat, 10 Nov 2010 00:00:00 EST -05:00
travel_to Time.zone.local(2020, 10, 1, 00, 00, 00)
Time.current # => Wed, 1 Oct 2020 00:00:00 EST -05:00
3) travel_back
Returns to the original time, by removing the stubs added by travel
and travel_to
.
Time.current # => Sat, 09 Nov 2020 00:00:00 EST -05:00
travel_to Time.zone.local(2020, 10, 20, 00, 00, 00)
Time.current # => Wed, 20 Oct 2020 00:00:00 EST -05:00
travel_back
Time.current # => Sat, 09 Nov 2020 00:00:00 EST -05:00
4) freeze_time
You can call this method to freeze the time
Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
freeze_time
sleep(1)
Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
Alternatively, this method also accepts a block and freezes the time inside the block.
freeze_time do
item = Item.create(name: "Chocolate", expiration_date: Time.current)
expect(post.published_at).to eq(Time.current)
end
In conclusion, TimeHelpers is a helper module that is supported natively in the Rails framework. This eliminates the need to bring in third party libraries such as the TimeCop gem. The TimeHelpers is a convenient helper module that allows us to manipulate date/time within our testing environment. Whether you are looking to manipulate the date/time into a timestamp of the future or past, or perhaps freezing time, the TimeHelpers module proves to be a viable tool at your disposal.
2 thoughts on “Working with Dates and Times in Rails RSpec testing”
Thanks for sharing.
I would add line: require ‘test_helper’
Awesome dispatch! I am indeed getting apt to over this info, is truly neighborly my buddy. Likewise fantastic blog here among many of the costly info you acquire. Reserve up the beneficial process you are doing here.