Time Flies While You're Having Fun Testing

Posted by Rick DeNatale Wed, 18 Jul 2007 19:45:00 GMT

I’ve been working on adding support for localization of the user’s time zone on an existing Rails app for a client. In order to test this, I found myself building a time machine.

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
end

The 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
end

The 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

  1. 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...

Comments

  1. Florian Groß 7 days later:

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