TLDR; Since Ruby on Rails 5.2 timecop gem can be replaced by built-in methods defined within the ActiveSupport::Testing::TimeHelpers module. Please remember about unfreezing time in tests, regardless of an approach you choose.

Sooner or later each of us encounters a situation where a method depends on time. The feature needs to be tested later on. Among Rubyists, the most popular gem which provides handy helpers to this problem is called timecop :

A gem providing “time travel”, “time freezing”, and “time acceleration” capabilities, making it simple to test time-dependent code. It provides a unified method to mock Time.now , Date.today , and DateTime.now in a single call.

To better illustrate when the gem may be useful, let’s write down some naive lines of code representing a building with a clock ⏰:

class Building def clock Time.zone.now end end

To cover the #clock method with rspec we could use the below lines:

describe Building do describe '#clock' do it 'returns time displayed by the clock' do expect(described_class.new.clock).to eq Time.zone.now end end end

And, surprisingly or not, the test didn’t pass:

expected: 2020-03-11 19:26:14.958265000 +0000 got: 2020-03-11 19:26:14.958228000 +0000

You know what they say – time flies when you’re having fun 💃.

The timecop gem provides a set of useful method to handle such cases without a need for complicated mocking time-related objects, inter alia:

Timecop.freeze to freeze time (which optionally accepts a block), Timecop.return to unfreeze time, Timecop.travel to time travel.

To make the failing tests, we can freeze time:

describe Building do describe '#clock' do it 'returns time displayed by the clock' do Timecop.freeze do expect(described_class.new.clock).to eq Time.zone.now end end end end

And tada 🎉, the test passed:

Building #clock returns time displayed by the clock Finished in 0.62 seconds (files took 2.63 seconds to load) 1 example, 0 failures

Timecop.freeze without a block and forgetting about Timecop.return to put time back the way it was. A common issue I have observed over time is callingwithout a block and forgetting aboutto put time back the way it was. Today I learned that there is a Timecop.safe_mode method which forces using the block syntax. Otherwise Timecop::SafeModeException is raised 🤓.

And that had been, more or less, an approach we took until Ruby on Rails 5.2 was released. According to its release note it adds freeze_time helper which freezes time to Time.now in tests.

Adopting the new helper(s)

In addition to the freeze_time there are also other useful methods grouped into the ActiveSupport::Testing::TimeHelpers module. They provide the same functionalities timecop provides. To make usage of them, we had to:

Find all occurrences of timecop methods in test files.

We found several places where time had been frozen but wasn’t unfrozen afterwards 👮 Replace them by equivalent from the TimeHelpers module. Remove timecop from Gemfile ✂️. Have one external dependency less :-).

The module is not included by default. To have the method available across tests files you need to include it, e.g. inside spec/rails_helpers.rb file:

config.include ActiveSupport::Testing::TimeHelpers .

To provide an example, let’s update the Building#clock? test:

include ActiveSupport::Testing::TimeHelpers describe Building do describe '#clock' do it 'returns time displayed by the clock' do freeze_time do expect(described_class.new.clock).to eq Time.zone.now end end end end

BONUS Making the code more consistent

To learn from our own mistakes, along the way we decided to unify our approach to time across all the test files. We made usage of the around RSpec hook to define a custom test helper (placed within spec/support/time_helper.rb file):

# Allow to freeze the time in scope of tagged example. RSpec.configure do |config| config.around(:each, :stop_the_time) do |example| freeze_time do example.run end end end

Thanks to it, we can simply freeze time by marking a test with :stop_the_time tag:

include ActiveSupport::Testing::TimeHelpers # Allow to freeze the time in scope of tagged example. RSpec.configure do |config| config.around(:each, :stop_the_time) do |example| freeze_time do example.run end end end describe Building do describe '#clock' do it 'returns time displayed by the clock', :stop_the_time do expect(described_class.new.clock).to eq Time.zone.now end end end

One additional benefit comes from the above approach: it uses the block syntax so there is no need to remember about unfreezing time each time test relies on it. Just use the tag aka stay consistent.

Summary

It is a good practice to review release notes of a new version of whatever you upgrade, language, framework or library. They often include important announcements like deprecation warnings and promote new features.

Thanks to new methods provided in Ruby on Rails 5.2 we made code of our tests better and decoupled an application from external dependency. We killed two birds with one stone 🙂