Friday, September 25, 2009

How to Temporarily Change Time.now and Unmock Time.now Using Mocha

Using a solution from stackoverflow to use Mocha to mock Time.now and a comment from Jacob describing how to unmock a specific method in Mocha, I generified Jacob's method, and I put the following into test/test_helper.rb of a Rails project that allows you to temporarily mock Time.now:
class TimeChange
  # author: Gary S. Weaver
  # from: http://stufftohelpyouout.blogspot.com/2009/09/how-to-unmock-in-mocha-and-temporarily.html
  # see also: http://stackoverflow.com/questions/1215245/ruby-unit-testing-how-to-fake-time-now
  
  def self.to(time)
    Time.stubs(:now).returns(time)
  end

  def self.back_to_now
    UnMocker.unmock(Time, 'now')
  end
end

class UnMocker
  # author: Gary S. Weaver
  # from: http://stufftohelpyouout.blogspot.com/2009/09/how-to-unmock-in-mocha-and-temporarily.html
  # based on solution from Jacob in http://szeryf.wordpress.com/2007/11/09/unstubbing-methods-in-mocha/

  def self.unmock(class_or_instance, method_name)
    Mocha::Mockery.instance.stubba.stubba_methods.each do |meth|
      if meth.stubbee == class_or_instance && meth.method == method_name
        meth.unstub
      end
    end
  end
end

Example of use:

$ script/console
Loading development environment (Rails 2.3.2)
>> require 'test/test_helper.rb'
=> [..., "TimeChange", "UnMocker"]
>> Time.now
=> Fri Sep 25 11:57:34 -0400 2009
>> TimeChange.to(Time.local(2009,"nov",2,23,59,59))
=> #<Mocha::Expectation:0x3800220 @return_values=#<Mocha::ReturnValues:0x380002c @values=[#<Mocha::SingleReturnValue:0x3800054 @value=Mon Nov 02 23:59:59 -0500 2009>]>, @backtrace=["/path/to/project/vendor/gems/mocha-0.9.5/lib/mocha/argument_iterator.rb:15:in `call'", "/path/to/project/vendor/gems/mocha-0.9.5/lib/mocha/argument_iterator.rb:15:in `each'", "/path/to/project/vendor/gems/mocha-0.9.5/lib/mocha/object.rb:94:in `stubs'", "./test/test_helper.rb:42:in `to'", "(irb):4:in `irb_binding'", "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/irb/workspace.rb:52:in `irb_binding'", "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/irb/workspace.rb:52"], @yield_parameters=#<Mocha::YieldParameters:0x3800108 @parameter_groups=[]>, @invocation_count=0, @cardinality=#<Mocha::Cardinality:0x3800090 @maximum=Infinity, @required=0>, @method_matcher=#<Mocha::MethodMatcher:0x38001e4 @expected_method_name=:now>, @mock=Time, @side_effects=[], @ordering_constraints=[], @parameters_matcher=#<Mocha::ParametersMatcher:0x38001bc @matching_block=nil, @expected_parameters=[#<Mocha::ParameterMatchers::AnyParameters:0x3800180>]>>
>> Time.now
=> Mon Nov 02 23:59:59 -0500 2009
>> TimeChange.back_to_now
=> [#<Mocha::AnyInstanceMethod:0x380e398 @method="find_by_uid", @stubbee=EasyLDAP::Connection>, #<Mocha::ClassMethod:0x38006f8 @method="now", @stubbee=Time>]
>> Time.now
=> Fri Sep 25 11:58:29 -0400 2009

Note that as soon as a test is done, Mocha should already unmock things, so you probably shouldn't need to do anything but a simple Time.stubs(:now).returns(your_time) to temporarily change time.

However, if you do want to mock and then unmock something (before Mocha would have unmocked it), this works.

References:
* http://stackoverflow.com/questions/1215245/ruby-unit-testing-how-to-fake-time-now
* http://szeryf.wordpress.com/2007/11/09/unstubbing-methods-in-mocha/

No comments: