loading...
Cover image for "Back to the future" or how to test time-based logic in Rails

"Back to the future" or how to test time-based logic in Rails

pashagray profile image Pavel Tkachenko ・3 min read

"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.

Posted on by:

pashagray profile

Pavel Tkachenko

@pashagray

Ruby Fullstack Developer. In love with Ruby and Rails. Sometimes happy with JS.

Discussion

markdown guide