Timecop vs Rails TimeHelpers

Posted by on March 25, 2021

TL;DR – You probably can’t replace Timecop with Rails’ built in TimeHelpers, as TimeHelpers only recreates Timecop’s freeze method, and can’t handle nested travelling.

Timecop is the go-to gem for testing time-dependent code, as it allows you to manipulate the current time during test runs. This is important because without control over the time, flakey tests can emerge in your codebase.

A very simple example is testing the created_at attribute of an ActiveRecord model:

# ActiveRecord will set the model’s created_at field
# to the current time on create
model = SomeModel.create!

expect(model.created_at).to eq(Time.now)

Most of the time the above test would pass without issue. However if this test happened to run just as the system clock ticked over to the next second, the model’s created_at timestamp would be one second earlier than Time.now in the expect comparison, causing the test to fail. A classic hard to reproduce flakey test.

This flakiness can be eliminated by including Timecop and calling Timecop.freeze at the beginning of this test. This will cause the current time to be constant throughout the test, regardless of what happens with the system clock. 

Timecop serves us well at FreeAgent, we use it extensively with over 1,300 calls to Timecop across more than 500 spec files.

It’s not just us though. Timecop is well used, and well loved, throughout the Ruby community. Since its release in 2009 it has been downloaded over 81 million times according to rubygems.org, which is particularly impressive considering it has one single maintainer.

Seemingly less well known, however, is that Ruby on Rails includes its own Timecop alternative with the equally catchy name of “ActiveSupport::Testing::TimeHelpers”.

TimeHelpers was released with little fanfare in Rails 4.1, by way of a brief sentence at the bottom of the release notes in April 2014. So it’s no surprise that not many Rails developers are aware of its existence. And that includes me, as I only discovered it this week.

While searching for the Timecop documentation, I happened upon a blog post entitled “Replace Timecop With Rails’ Time Helpers in RSpec”. This instantly grabbed my attention as we have over 300 gems in our majestic monolith and any opportunity to remove a dependency is a good thing.

I spent some time getting acquainted with the TimeHelpers documentation, and it seemed on the surface that the rumors were true – TimeHelpers does indeed contain its own versions of almost all of Timecop’s features:

Timecop methodTimeHelpers equivalent
freezefreeze_time
traveltravel / travel_to
returnunfreeze_time / travel_back
scaleN/A

The only difference is Timecop has Jean‑Claude Van Damme a scale method, which allows you to change the speed at which time passes. But three out of four ain’t bad.

Unfortunately, all is not as it seems, as I discovered upon digging deeper into TimeHelpers.

Ignoring the scale method, for which there is no equivalent in TimeHelpers, both libraries offer two ways to manipulate time: freeze and travel.

These two concepts seem self explanatory: freeze stops time, and travel offsets the current time by a given amount. That’s how Timecop works at least.

TimeHelpers sees things a little differently. Freeze does indeed freeze time. However TimeHelpers’ travel methods also freeze time with the addition of an offset.

There is no way to just travel in time without also freezing it when using TimeHelpers. This is because all the TimeHelpers’ methods really do is stub Time.now, Date.today, and DateTime.now to return the date and time specified.

Consider the following example:

include ActiveSupport::Testing::TimeHelpers

travel_to(Time.parse("2020-01-01"))

Time.current
# => 2020-01-01 00:00:00 +0000

sleep(10)

Time.current
# => 2020-01-01 00:00:00 +0000

I’d have expected the second call to Time.current to return a time 10 seconds later than the first, something like 2020-01-01 00:00:10 +0000. After all, I didn’t ask for time to be frozen, just to travel through it. It’s a reasonable assumption that time will keep on ticking after the traveling, especially as that is how Timecop works.

Unfortunately, this isn’t the only place that TimeHelpers fails to live up to Timecop. Timecop allows you to nest changes in time, eg:

Timecop.travel(Time.parse("2020-01-01")) do
  puts "First travel: #{Time.current}"

  Timecop.travel(1.day) do
    puts  "Second travel: #{Time.current}"
  end
end

# => First travel: 2020-01-01 00:00:00 +0000
# => Second travel: 2020-01-02 00:00:00 +0000

Where trying the same with TimeHelpers raises an error:

include ActiveSupport::Testing::TimeHelpers

travel_to(Time.parse("2020-01-01")) do
  puts "First travel: #{Time.current}"

  travel(1.day) do
    puts  "Second travel: #{Time.current}"
  end
end

# => RuntimeError (Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing.)

Much to my disappointment, TimeHelpers is not a drop-in replacement for TimeCop.

Having said that, I don’t think TimeHelpers is completely useless. In the majority of situations, freezing time is enough to reliably test time-dependent code. So if you’re starting a new Rails project, you can most likely forgo installing Timecop, and use TimeHelpers instead.

However, if you’re already using Timecop, it’s unlikely that you’ll be able to replace it with TimeHelpers.

This was the case for us at FreeAgent, as we make extensive use of Timecop’s travel functionality, as well as nested time travelling, so until TimeHelpers is updated to include those features, we’ll be sticking with Timecop.

There is still a happy ending to this story, as we were able to replace Timecop with TimeHelpers in our Dev Dashboard app, which is a separate Rails codebase for managing access and authentication with our API, and now has one less gem.

Leave a reply

Your email address will not be published.