
At first, I did a fairly simple hack which monkey patched the system methods Time.now, and Date.today. This worked until I got into some testcases of code which was triggered off of updated_at fields in various ActiveRecord models. Since I had to deal with these implicit dates, I found that I needed to have finer control than my simple patch gave.
The code has now evolved to the point where I think that it shows some interesting aspects of basic Ruby metaprogramming.
The Initial Approach
In order to control the time, I decided to add a module in Test::Unit::Testcase called TimeMachine, so that a testcase could just include TimeMachine when it needed the function.
The way it’s used is to write something like:
def testSomethingYesterday
now_as(1.days.ago) do
#code to be run yesterday here
end
endThe now_as method takes a time as a parameter. Within the block, Time.now will return that time, and Date.today will return the date corresponding to that time.
Doing this was fairly straightforward. The method aliased the two methods, redefined them, then, within a begin block with an ensure clause to restore the methods afterwards, yielded to the block.
Non-stop Time Trip Only
While this simple approach worked well, it had one drawback. It couldn’t be stacked. If you called now_as again within the block, the inner call would remove the monkeypatched methods when it returned. This first showed up when I had a bug in one of my testcase methods. That was fixed easily enough by rewriting that test.
But when I ran into the code which was using implicit times, I figured it would be easier to make my test helper a bit more sophisticated. I needed a time machine which could make side-trips along it’s round, trip in time.
The TimeMachine as of Now
So here’s my current implementation of Test::Unit:Testcase::TimeMachine:
class Test::Unit::TestCase # 1
module TimeMachine
def now_as(time)
time_class = class << Time; self; end # 5
date_class = class << Date; self; end
begin
Time.class_eval do
@now_stack ||= []
if @now_stack.empty? # 10
time_class.class_eval do
alias_method :old_now, :now
def now
@now_stack.last.dup
end # 15
end
date_class.class_eval do
alias_method :old_today, :today
def today
Time.now.to_date # 20
end
end
end
@now_stack.push(time.dup)
end # 25
yield
ensure
Time.class_eval do
@now_stack.pop
if @now_stack.empty? # 30
date_class.class_eval {alias_method :today, :old_today}
time_class.class_eval {alias_method :now, :old_now}
end
end
end # 35
end
endThe basic idea is to maintain a stack of times in a class instance variable of Time. We define the pseudo now and today methods when the first time is placed on this stack, and restore them when the last time is removed.
The tricky part of this code is knowing when to talk to the Time and Date classes and when to talk to their respective metaclasses. In lines 5 and 6 I grab the two metaclasses so that I can refer to them in the code below (DRY). In line 7 I start the begin block which ensures that things will be restored when now_as has finished.
On line 9, running in the context of the Time class, I ensure that it has an instance variable to contain the stack of now times. Then if the stack is empty, I define the methods. Lines 12-15 run in the context of Time’s metaclass to define now as a class method of Time. Lines 18-21 handle the today method in a similar fashion.
After we’ve ensured that our newly patched methods are there, we push the time on line 24.
The yield, on line 26 runs back in the context of the testcase, which proceeds to do it’s thing.
Once that’s done, succeed or not, the ensure block cleans things up. Lines 29-33 go back to the context of the Time class to manipulate the stack and, if it’s empty again, restore the original methods.
What’s in the Future?
So that’s where the time machine sits right now. It does the simplest thing that could possbily work, right now. It might be nice if it could, perhaps optionally, have the current time change rather than being fixed, in other words, Time.now would return the originally stated time plus whatever time increment had elapsed since the as_now call. But for now, I haven’t needed it, and I’m not planning to use the time machine to artifically skip ahead to find future requirements before I discover them in real time.
Postscript
After posting this yesterday, I discovered that the nesting didn’t really work. I was only pushing the time on to the stack the first time. I’ve just corrected the code above.
Trackbacks
Use the following link to trackback from your own site:
http://talklikeaduck.denhaven2.com/trackbacks?article_id=445
-
Yesterday I wrote about some code I wrote to control what Time.now and Date.today return to support time-dependent testing. This afternoon I discovered a critical, but easy to fix bug. I’ve updated the original article. The good news is that...





Heh, that reminds me of http://flgr.0x42.net/code/good_time.rb which was written a good while ago. :)