TIL: Timecop has a safe mode
Tags: ruby, rails, testing
Published: 2022-04-13 23:20:18

We use Timecop at work as a means of easily mocking the current date and being able to traverse around to simulate shifts in time. It's been an indispensable tool for testing out some particularly complex, time-sensitive components of our product.

Something a little inconsistent throughout the codebase has been people's preference between using Timecop's block or not. The latter relies on the developer to put time back to normal by calling Timecop.return after they’re done.

## Lost in Time

Given that both `#freeze` and `#travel` mess with the app's understanding of what `Time.now` is at any given moment, what happens if you don't return time to normal?

This recently popped up as the cause of a few flakey tests that would rear their head in CI. We have a lot of tests in different areas that are sensitive to time. One, in particular, was popping up as a flake that was testing that a timestamp representing the last time a record was accessed was being updated. The flake that was popping up was failing intermittently because the timestamp was _not_ changing, even though after some initial poking around it seemed like the actual functionality was behaving just fine otherwise.

It stood out to me that the failed assertion was seeing a very event time not being set, one that didn't involve any seconds or milliseconds. Having a quick search for that DateTime, I found another unrelated set of tests that were set up something like the following:

```ruby
context 'context 1' do
  before { Timecop.freeze(some_time) }
  after { Timecop.return }
end


context 'context 2' do
  before { Timecop.freeze(some_time) }
end
```

So we had a context being set up in our test suite that was freezing time, but not putting it back to normal so that frozen time was leaking out and impacting other tests.

## Enter Safe Mode

With things from [my last post](/til-queuing-sidekiq-workers-safely-with-the-help-of-isolator/) in mind, I really didn't want this to become another thing where we were like, "you need to know about this gotcha forevermore whenever you're going to use Timecop". Luckily, a quick look at the docs showed that the library has [a safe mode feature](https://github.com/travisjeffery/timecop#timecopsafe_mode) that enforces block syntax use. You can add this in your spec helper with the below:

```ruby
Timecop.safe_mode = true
```

Adding this raised several errors in the test suite in places that were set up like the above contexts:

```ruby
Timecop::SafeModeException:
       Safe mode is enabled, only calls passing a block are allowed.
```

With the tests failing, we now had a list of tests that we needed to go and amend and use the block syntax where necessary. A basic version of what that might look like is:

```ruby
it 'does something time-sensitive' do
  Timecop.freeze(some_time) do
    # Some test setup and assertions
  end
end
```

At the end of your test, time will be back to the way it was without having to call `Timecop.return`. This can get a little monotonous if you have a lot of tests in one area that is all time-sensitive, but you can still achieve something similar to `context 1` above using the setup hooks.

```ruby
context 'context 1' do
  around do |example|
    Timecop.freeze(some_time) { example.run }
  end
end
```

I was pretty happy with how this turned out, a rather insidious cause for flakey tests had been ironed out, and the issue of consistency between the two ways of using the library was solved by being forced to use one of them.