"Back to the future" or how to test time-based logic in Rails
Tags: ruby, rails, rspec, tdd
Published: 2020-07-21 15:54:02

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

```ruby
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.

```ruby
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.

```ruby
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.

```ruby
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`.

```ruby
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. 

```ruby
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. 

```ruby
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.