TLDR; Since Ruby on Rails 5.2
timecop gem can be replaced by built-in methods defined within the
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
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
DateTime.nowin 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 💃.
timecop gem provides a set of useful method to handle such cases without a need for complicated mocking time-related objects, inter alia:
Timecop.freezeto freeze time (which optionally accepts a block),
Timecop.returnto unfreeze time,
Timecop.travelto 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.freezewithout a block and forgetting about
Timecop.returnto 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
timecopmethods in test files.
We found several places where time had been frozen but wasn’t unfrozen afterwards 👮
- Replace them by equivalent from the
- Have one external dependency less :-).
To provide an example, let’s update the
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
# 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
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.
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 🙂