Time is a huge topic. I mean, since the start of the times. Our species has thinking about time since a millennia ago. We don’t know the precise date when that happened because we didn’t take account of time until we ponder a lot about it. So, we could sometimes overlook some nuances of time. And that happened to the team that wrote some tests in a Rails project.
I started collaborating on that project where I took a tech-lead role. I was performing more code reviews than coding. And one of the first things that popped up to my eye when reviewing pull requests was that the team was changing tests even when the change was a refactor.
“Why are you rewriting tests for a refactor? In a refactor, tests should remain the same. That tells us that the refactored code is working in the same way as the older code! ” I added in my comments.
The frustrated team members pointed out that each week, at least one test fails because it depends on the date. One app controller retrieves the results of people whose birthday occurs on that week. If they run the test suite in a different week, they need to update the expected result.
I was shocked! I was planning to add a task for refactoring those tests until the product owner said, “No! On this point, we need to create stories that add value to the final user. Tests are important, but they don’t add value to the final user!”
I didn’t want to argue, so I accepted the ridiculous reality with stoicism. I continued performing code reviews, and I learned to ignore changes on tests.
But after a while, I found the time to tackle those disgraceful test cases. And I found the gem «timecop», a gem created by John Trupiano and maintained by Travis Jeffery.
Timecop is a ruby gem created for those specific cases where we have tests dependent on time. It could be birthdays, but also it could be expiration days for JWT tokens or recurrent billings.
In a single line, Timecop creates a mock of Time.now, Date.today, and DateTime.now. As Timecop creators highlight in their README file, the gem doesn’t have dependencies, so it works well with any Ruby test framework.
With only two basic methods (Time.travel
and Time.freeze
), you can easily control the time!
Timecop is a ruby gem available on rubygems.org. So, you can just add Timecop to your Gemfile and run bundle install
after that. Unless you're using Timecop for something different to testing, add the gem in a test group.
Gemfile
...
group :test do
... # Other gems
gem 'timecop'
end
Alternatively, run bundle add timecop
or install it on your computer with gem install timecop
.
As I commented above, you only need to know two methods for using Timecop: Timecop.travel and Timecop.freeze. But what is the difference between these two methods?
With Timecop.travel
, you set up time to a specific date, and then time will move forward after that.
For example, if you Timecop.travel
to February 2, 1993, 6:00:00 AM, after ten seconds, Time.now
returns February 2, 1993, 6:00:10 AM
groundhog_day = Time.local(1993, 2, 2, 6, 0, 0)
Timecop.travel(groundhog_day)
sleep(10)
groundhog_day == Time.now # ==> false
On the other way, with Timecop.freeze
time stops completely. So, using a similar example, Time.now
will return exactly on February 2, 1993, 6:00:00 AM:
groundhog_day = Time.local(1993, 2, 2, 6, 0, 0)
Timecop.travel(groundhog_day)
sleep(10)
groundhog_day == Time.now # ==> true
Other Timecop methods allow you to move time faster or return to the system date.
Move times faster. It’s a good option for integration tests where you want to check that some jobs are performed. Code example:
# seconds will now seem like hours
Timecop.scale(3600)
Time.now
# => 2012-09-20 21:23:25 -0500
# seconds later, hours have passed and it's gone from 9pm at night to 6am in the morning
Time.now
# => 2012-09-21 06:22:59 -0500
Move time to system date again.
GROUNDHOG_DAY = Time.local(1993, 2, 2, 23, 59, 51)
def groundhog_day? = Time.now == GROUNDHOG_DAY
Timecop.freeze(GROUNDHOG_DAY)
sleep(10)
groundhog_day? # => True
Timecop.return
groundhog_day? #=> False
Timecop readme is really clear for its usage. For using Timecop in a time-sensitive test:
joe = User.find(1)
joe.purchase_home()
assert !joe.mortgage_due?
# move ahead a month and assert that the mortgage is due
Timecop.freeze(Date.today + 30) do
assert joe.mortgage_due?
end
For use in a group of tests, you write:
describe "some set of tests to mock" do
before do
Timecop.freeze(Time.local(1990))
end
after do
Timecop.return
end
it "should do blah blah blah" do
end
end
If you want to set up your test environment to always start on the same date, add this configuration:
config/environment/ test.rb
... # your configuration
config.after_initialize do
# Set Time.now to February 2, 1993 06:00:00 AM (at this instant), but allow it to move forward
# year, month, day, hour, min, sec
t = Time.local(1993, 2, 2, 6, 0, 0)
Timecop.travel(t)
end
...
end
After that, each time that you run your test, the date will start on February 2 at 6:00 AM.
That’s all! I hope you found Timecop as useful as this post.