Til the End of Time (or Time.now)

July 19, 2007 Nathan Wilmes

Sooner or later, every test-driven developer discovers that they need a superpower – the power to control time. Let’s say you’re working on a scheduling system. You’re going to want to write tests that say, “Assume that it’s 2PM on January 3rd and this user does THIS. What happens as a result? What happens at 4PM on January 3rd as a result?”

One of the joys of writing in Rails is the sheer power you have to change your application universe at almost any level you choose. As a result, we’ve come up with a variety of ways to solve this problem. As it turns out, with great power comes great responsibility. (I’m not sure why I’m going all comic book right now, but let’s run with it).

Stubbing Time

One of the cooler features of the latest generation of mocking frameworks like FlexMock and Mocha is the ability to stub any method you choose and replace its functionality with a new one of your choosing.. and then the framework will clean up after you when the test is over!

This leads us to some intriguing possibilities such as the following:

def test_party
  Time.stubs(:now).returns(Time.local(1999, 'Jan', 1))
  people(:nathan).party
  assert people(:nathan).very_drunk?
end

and for the duration of the test, Rails will decide that it needs to party like it’s 1999. Sounds great, right?

Well, there’s just a teeny catch.

Let’s try benchmarking the test_party method.

Benchmark.measure do |b|
  ... test_party
end

And you’ll discover that this test takes -2000000 seconds to run!

Here’s another problem area:

def test_partying_in_selenium
  Time.stubs (:now).returns(Time.local(1999, 'Jan', 1))
  click "link=Party!"
  wait_for {get_text("id=sobriety") == "drunk"}
end

The issue with this method is that the wait_for method relies on Time.now to time out.. and Time has now been mocked out for the duration of the test. So we end up with a Selenium test that takes forever.

So doing a mass Time.now stub will work, but stubs out the test framework along with your application. Not a great solution.

How about this?

A Time wrapper for your application

The idea here is that we define a time wrapper class that we use for our application.. let’s call it Clock. The idea of the Clock object is that the application refers to the Clock every time it needs to know what time it is. Clock would have a very similar API to Time.. we can use Clock.now instead of Time.now, and switching over is as simple as a global search and replace.

Clock.now does exactly the same thing as Time.now… except when you’re running tests. When you run tests, you suddenly get access to clock-controlling methods, and can change what the application’s sense of ‘now’ is for the duration of a test.

Here’s a simple implementation of Clock:

in lib/clock.rb:

class Clock
  def self.now
     Time.now
  end
end

in mocks/test/lib/clock.rb:

class Clock
  def self.now
     @@now ||= Time.now
  end

  def self.now=(new_time)
     @@now = new_time
  end
end

To finish off, here’s example test code that uses this:

def test_party
  Clock.now = Time.local(1999, 'Jan', 1)
  people(:nathan).party
  assert people(:nathan).very_drunk?
end

def teardown
  Clock.now = Time.now
end

Some other benefits to this approach is that you have a handy place to add fancier setters to Clock. You can use methods like Clock.tick(2.hours), Clock.advance_to_midnight, et cetera. The actual implementations of these methods would be very simple, and I’ll leave them as an exercise for the reader who wants to use them.

Here’s some issues you’ll run into, though:

  • The implementation I wrote above doesn’t automatically tear down the Clock time override after your test is done. As a result, you’ll have to do this teardown yourself. If you don’t, later tests will think that Clock.now is whatever the previous tests say it is, and you may end up with fragile interactions between tests as a result. Very bad.
  • Rails has many lower-level methods that use Time.now directly, and these methods don’t easily switch over to Clock.now. Among the more interesting ones are the created_at and updated_at methods, and constructs like 3.days.from_now and 5.hours.ago. So, you’d need workarounds to test these methods.

Here’s a case that would work:

def Clock.ago(duration)
  Clock.now - duration
end

def Clock.from_now(duration)
  Clock.now + duration
end

Using stubs rather than a separate mock clock class

Why do we need to create an ‘Clock.now=XXX’ method, anyways? If we use stubs to override what AppTime.now returns, we can let the test framework clean up after itself when the test is complete. In addition, we can drop the mock definition entirely.. a very good thing indeed.

So in this universe, we’d end up with something like this:

Here’s a simple implementation of Clock:

in lib/clock.rb:

class Clock
  def self.now
     Time.now
  end
end

in mocks/test/lib/clock.rb: nothing!! this file doesn’t exist!

Test code:

def test_party
  Clock.stubs(:now).returns(Time.local(1999, 'Jan', 1))
  people(:nathan).party
  assert people(:nathan).very_drunk?
end

This approach has the advantage of reducing the number of ‘special’ test-only objects you have, and working more closely with the objects your application uses. The disadvantage is that you can no longer add lots of test-only helper functions to describe how you’re playing with the clock more clearly.. at least, not in the ‘Clock’ object.

Can we change Rails to use a settable clock?

The Rails code refers to Time.now in many, many places (just try a global search of your gems directory, and you’ll see what I mean.) At this point, the hooks are not in convenient places to allow for a settable clock.. you would have to copy entire method signatures and tweak some Time.now references, which ties your changes to a particular Rails install and creates unfortunate forks in your code. So, I would say that the answer for now is, sadly, “No”.

About the Author

Biography

Previous
Access Control & Permissions in Rails
Access Control & Permissions in Rails

Access Control is a simple idea. We want company employees to be able to delete inappropriate content; but ...

Next
Creating Multiple Models in One Action
Creating Multiple Models in One Action

One of the issues in my previous post The Controller Action that sparked some interest is the handling of t...