Tuesday, November 13, 2007

Ruby: Time::is

Mocha is fantastic for unit testing, but I usually try to avoid requiring it while functional testing. In general this works, but Time.now is something that I occasionally like to fix even while functional testing. To solve the problem, within a functional test helper I load a time_extensions.rb file that defines a Time::is method. The Time::is method is useful for freezing time at a certain point and executing a block of code. When the block of code finishes the Time.now method is returned to it's original implementation.

The example below is how I usually solve the described problem.

require 'time'

class Time
def self.metaclass
class << self; self; end
end

def self.is(point_in_time)
new_time = case point_in_time
when String then Time.parse(point_in_time)
when Time then point_in_time
else raise ArgumentError.new("argument should be a string or time instance")
end
class << self
alias old_now now
end
metaclass.class_eval do
define_method :now do
new_time
end
end
yield
class << self
alias now old_now
undef old_now
end
end
end

Time.is(Time.now) do
Time.now # => Tue Nov 13 19:31:46 -0500 2007
sleep 2
Time.now # => Tue Nov 13 19:31:46 -0500 2007
end

Time.is("10/05/2006") do
Time.now # => Thu Oct 05 00:00:00 -0400 2006
sleep 2
Time.now # => Thu Oct 05 00:00:00 -0400 2006
end

9 comments:

  1. Nice trick. Is there a reason you don't put "alias old_now now" inside the first class_eval block?

    ReplyDelete
  2. Looking at the code, I don't see any reason why you couldn't.

    Cheers, Jay

    ReplyDelete
  3. You should have an "ensure" after the "yield" so that if the test fails Time.now will go back to the correct method.

    ReplyDelete
  4. my tests never fail. ;)

    ReplyDelete
  5. Why not juse use mocha? I would argue it will be easier to read and follow for future devs, especially since it would be a 1 liner versus opening up the metaclass and hacking it manually.

    ReplyDelete
  6. Anonymous5:40 AM

    Jay, could you bless us with a little insight on why you jump through the metaclass hoop instead of redefining now() right after creating the old_now alias?

    ReplyDelete
  7. I believe you are talking about this:

    class << self
    alias old_now now
    end
    metaclass.class_eval do
    define_method :now do
    new_time
    end
    end

    which could also be

    metaclass.class_eval do
    alias old_now now
    define_method :now do
    new_time
    end
    end

    There is no reason not to use the 2nd version. The first version was simply how the code evolved as I was writing tests. The 2nd version is what I would refactor to after all the tests were passing, I just missed it in the initial write up.

    Cheers, Jay

    ReplyDelete
  8. Anonymous8:30 PM

    Why would you not just get rid of the metaclass call and just define now again in the first class << self ?

    I'm curious about what the purpose of metaclass is.

    ReplyDelete
  9. If you redefined now like so:

    class << self
    alias old_now now
    define_method :now do
    new_time
    end
    end

    then "new_time" wouldn't be defined. The class << self limits the scope.

    Cheers, Jay

    ReplyDelete

Note: Only a member of this blog may post a comment.