Working with Dates and Times in Rails RSpec testing

rspec_blog

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.

Share on twitter
Twitter
Share on telegram
Telegram
Share on facebook
Facebook
Share on linkedin
LinkedIn
Share on email
Email

Leave a Comment

Your email address will not be published. Required fields are marked *