⚠️ Archived Post
This is an archived post from my previous blog (2007-2014). It may contain outdated information, broken links, or deprecated technical content. For current writing, please see the main Writing section.

Rails Time Travel Revisited: Manipulating Time.now for Specs

Originally published on October 14, 2008

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