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 sleep 2
Time.now end
Time.is("10/05/2006") do
Time.now sleep 2
Time.now end
Nice trick. Is there a reason you don't put "alias old_now now" inside the first class_eval block?
ReplyDeleteLooking at the code, I don't see any reason why you couldn't.
ReplyDeleteCheers, Jay
You should have an "ensure" after the "yield" so that if the test fails Time.now will go back to the correct method.
ReplyDeletemy tests never fail. ;)
ReplyDeleteWhy 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.
ReplyDeleteJay, 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?
ReplyDeleteI believe you are talking about this:
ReplyDeleteclass << 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
Why would you not just get rid of the metaclass call and just define now again in the first class << self ?
ReplyDeleteI'm curious about what the purpose of metaclass is.
If you redefined now like so:
ReplyDeleteclass << 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