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
andupdated_at
methods, and constructs like3.days.from_now
and5.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