"Back to the future" or how to test time-based logic in Rails
If time travel is possible, where are the tourists from the future? - Stephen Hawking
Almost every app works with time-based logic. How can you test that the post is outdated, bills are already expired or user is late for the plane. In this post you will learn RSpec approaches to test such a time-rich logic.
Let's start from simple example. We have model Post
with attribute published_at
and we want a method outdated?
, which returns true if post was published more than one week ago.
class Post < BaseModel
def outdated?
published_at < Time.current - 7.days
end
end
Let's try to test it properly. The simplest solution is to create post, which is published more than 7 days ago and invoke this method.
describe Post do
describe "#outdated?" do
context "when more than a week has passed since publishing" do
subject { Post.create(published_at: Time.current - 8.days) }
it "returns true" do
expect(subject.outdated?).to eq(true)
end
end
end
end
But sometimes it is hardly achievable, especially when we need to be at the particular point in time. Let's assume that post can not be published on Friday. Everyone is drinking and can publish something what one will regret all their life, I am sure you know.
def can_be_published?
!Time.current.friday?
end
And now to test such case for false
return, we need to be in the particular point in time -- a Friday. You can achive that with a help of timecop
gem.
describe Post do
describe "#can_be_publised?" do
context "when current date is friday" do
# Set current time to friday
Timecop.freeze(Time.new(2020, 6, 10)) do # 2020/06/10 is a Friday
it "returns false (can't be published)" do
expect(subject.can_be_publised?).to eq(false)
end
end
end
context "when current date is Saturday" do
Timecop.freeze(Time.new(2020, 6, 11)) do # 2020/06/11 is a Saturday
it "returns true" do
expect(subject.can_be_publised?).to eq(true)
end
end
end
end
end
As you can see we can jump to the particular point in time with timecop
gem. Isn't that cool? I suggest to rewrite spec for outdated?
method using timecop
.
describe Post do
describe "#outdated?" do
subject { Post.new(published_at: Time.current) }
context "when more than a week has passed since publishing" do
Timecop.freeze(Time.current + 8.days) do
it "returns true" do
expect(subject.outdated?).to eq(true)
end
end
end
end
end
Looks much more natural. We don't stuff our post attributes with other values but simply jump in time to check time-based logic. It becomes especially useful when time logic is really tricky and you need to show other developers that you are testing something in the future or in the past.
Another interesting feature is scaling the time. We can make it flow faster or slower. Imagine that you have some method which calls sleep for a limited time.
class ExternalApi
def emulate_throttling
# some logic
sleep(5)
# another piece of logic
sleep(5)
# and another one piece
end
end
You definetelly don't want to wait for 10 seconds for test to complete. With TimeCop we can reduce that time.
describe "#emulate_throtling" do
it "run some external API without bloating it with requests" do
Timecop.scale(100) # 100x speed up of time
subject.emulate_throttling
Timecop.scale(1) # return back
end
end
Look, we increased the speed a hundred times, so our test passes not in 10 seconds, but in 0.1 seconds. Awesome, isn't it?
As we can see time in Ruby is flexible and you can control it. TimeCop is a must have tool for testing your time-sensitive logic.
Top comments (0)