Rails Time Travel Revisited: Manipulating Time.now for Specs

Posted on 14 October 2008 by Johannes Fahrenkrug. Tags: Programming Ruby rails
A few month ago I wrote about how to manipulate Time.now for your Rails Test::Unit tests. I've switched to using RSpec since then and had the same problem again: I have code that uses Time.now and I want to test it. It was fairly easy to move the code from Test::Unit to RSpec:
  1. Copy the following code into spec/time_spec_helper.rb
    unless Time.respond_to? :real_now   # prevent the error: stack level too deep (SystemStackError)
    # <b>Test Helper: used only in testing!</b>
    #
    # Extend the  Time  class so that we can offset the time that  now
    # returns. This should allow us to effectively time warp for functional
    # tests that require limits per hour, what not.
    #
    # Example usage:
    #   require File.expand_path(File.dirname(__FILE__)   '/../spec_helper')
    #   require File.expand_path(File.dirname(__FILE__)   '/../time_spec_helper')
    #
    #   describe YourModel do
    # 
    #     before(:each) do
    #       pretend_now_is(Time.local(1999, 8, 1))  # position *all* tests back in time!
    #     end
    # 
    #     after(:each) do
    #       Time.reset    # jump back to the present
    #     end
    #
    #     it "should be 1999" do
    #       Time.now.year.should == 1999
    #     end
    #
    #     # If one particular spec needs some time jumping of its own...
    #     it "should jump to decades" do
    #       pretend_now_is(Time.local(1960)) do
    #         Time.now.year.should == 1960
    #       end
    #
    #       pretend_now_is(Time.local(1970)) do
    #         Time.now.year.should == 1970
    #       end
    #     end
    #     # ...
    #   end
    #
    #
    # <em><tt>(see reference http://snippets.dzone.com/posts/show/1738)</tt></em>
    class Time
      class <<self
        attr_reader :offset
        alias_method :real_now, :now
        def now
          @offset = 0 if @offset.nil?
          real_now - @offset
        end
        alias_method :new, :now
    
        # Warp to an absolute  time  in the past or future, making sure it takes
        # the present as the reference starting point when making the jump.
        def set(time)
          reset
          @offset = now - time
        end
    
        # Jump back to present.
        def reset
          @offset = 0
        end
      end
    end
    end
      
    # Time warp to the specified  time . If given a block, it applies only for the
    # duration of the passed block.
    def pretend_now_is(time)
    Time.set(time)
    if block_given?
      begin
        yield
      ensure
        Time.reset
      end
    end
    end
  2. Require it in your spec:
    require File.expand_path(File.dirname(__FILE__) + '/../time_spec_helper')
  3. Travel through time in your specs:
    before(:each) do
      pretend_now_is(Time.local(1999, 8, 1))  # position *all* tests back in time!
    end
    
    after(:each) do
      Time.reset    # jump back to the present
    end
    
    it "should be 1999" do
      Time.now.year.should == 1999
    end
    
    # If one particular spec needs some time jumping of its own...
    it "should jump to decades" do
      pretend_now_is(Time.local(1960)) do
        Time.now.year.should == 1960
      end
    
      pretend_now_is(Time.local(1970)) do
        Time.now.year.should == 1970
      end
    end
  4. Enjoy.
Please note that this code is based on this highly useful piece of code.

If this was useful for you, please take a minute and recommend me: Recommend Me Thank you!



Comments

Johannes Fahrenkrug said...

That's a great idea, Rick!
I'd still keep the "pretend_now_is" method, though, since it's so nice and readable, but changing the implementation to take advantage of stubbing will basically bring it down to a one-liner. Nice!

October 17, 2008 08:31 AM

rick said...

Why not just use your stubbing library of choice?

Time.stub!(:now).and_return(Time.utc(....))

October 16, 2008 06:35 PM

Comments

Please keep it clean, everybody. Comments with profanity will be deleted.

blog comments powered by Disqus