Never been to DZone Snippets before?

Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world

Time warp for functional and unit testing (See related posts)


Add this at the top of test/test_helper.rb (or elsewhere if not using rails):

# 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.
class Time #:nodoc:
  class <<self
    attr_accessor :testing_offset
    alias_method :real_now, :now
    def now
      real_now - testing_offset
    end
    alias_method :new, :now
  end
end
Time.testing_offset = 0


Add this method to Test::Unit::TestCase (in the class definition in test_helper.rb):

  # Time warp to the specified time for the duration of the passed block
  def pretend_now_is(time)
    begin
      Time.testing_offset = Time.now - time
      yield
    ensure
      Time.testing_offset = 0
    end
  end


And now you can write time-based tests. For example:

  def test_should_not_allow_more_than_3_requests_in_last_hour_from_same_ip
    (1..3).each { |n| successful_request }

    start_count = WorkOrderRequest.count
    post :new, :work_order_request => REQUEST_TEMPLATE
    assert_response :redirect
    assert_redirected_to :controller => 'work_order_request',
                         :action => 'limit_exceeded'
    assert_equal start_count, WorkOrderRequest.count
  end

  def test_should_not_allow_more_than_10_requests_in_last_24_hours_from_same_ip
    10.downto(1) do |n|
      pretend_now_is(n.hours.ago) do
        successful_request
      end
    end

    start_count = WorkOrderRequest.count
    post :new, :work_order_request => REQUEST_TEMPLATE
    assert_response :redirect
    assert_redirected_to :controller => 'work_order_request',
                         :action => 'limit_exceeded'
    assert_equal start_count, WorkOrderRequest.count
  end


Comments on this post

bassnode posts on Jan 08, 2007 at 23:41
nice! very helpful. though I needed something to change the date entirely so I modified it a bit:
class Time #:nodoc:
  class <<self
    attr_accessor :warp
    alias_method :real_now, :now
    def now
      warp
    end
    alias_method :new, :now
  end
end
Time.warp = Time.real_now

def pretend_now_is(time)
  begin
    Time.warp = time
    yield
  ensure
    Time.warp = Time.real_now
  end
end 

hugocf posts on Aug 23, 2007 at 23:17
I think I might have found a minor bug when you use a global <tt>Time.testing_offset</tt> different from 0. You need to make sure the <tt>pretend_now_is(time)</tt> is based on the current real time, by resetting the offset back to 0 before calculating the new offset:
      Time.testing_offset = 0


The full <tt>pretend_now_is(time)</tt> becomes:
  # Time warp to the specified time for the duration of the passed block
  def pretend_now_is(time)
    begin
      Time.testing_offset = 0
      Time.testing_offset = Time.now - time
      yield
    ensure
      Time.testing_offset = 0
    end
  end


A test case showing what I mean is this:

require File.dirname(__FILE__) + '/../../test_helper'

# To avoid the error: stack level too deep (SystemStackError)
# => http://bjhess.com/bjhessblog/2007/08/12/time-warp-for-rails-testing/
require File.dirname(__FILE__) + '/../../time_helper'

class WarpTest < Test::Unit::TestCase
  
  def setup
    Time.testing_offset = Time.now - Time.local(2007, 8, 15)  # position all test back in time!
  end
  
  # ... many, many test that do NOT USE the pretend_now_is()

  # but one particular test needs to make some time jumping of its own
  def test_time_travel_machine
    pretend_now_is(Time.local(2007, 8, 7)) do
      puts Time.now          # => 2007-08-16 ... NOT WHAT WE'D EXPECT!!!
    end
  end
end


bjhess posts on Aug 24, 2007 at 06:30
hugocf mentions my "stack level too deep" workaround in his comments above. Details here.

This was my error message in test_helper:

./test/functional/../test_helper.rb:13:in `real_now': stack level too deep (SystemStackError)


I moved all time specific code to a new time_helper.rb in /test:

ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")

# Extend the Time class so that we can offset the time that 'now'
# returns.  This should allow us to effectively time warp any
# tests that require limits per hour, specific time borders, etc.
class Time #:nodoc:
  class <<self
    attr_accessor :testing_offset
    alias_method :real_now, :now
    def now
      real_now - testing_offset
    end
    alias_method :new, :now
  end
end
Time.testing_offset = 0

class Test::Unit::TestCase

  # Time warp to the specified time for the duration of the passed block.
  # Include time_helper in your test code to use:
  #   require File.dirname(__FILE__) + '/../time_helper'
  def pretend_now_is(time)
    begin
      Time.testing_offset = Time.now - time
      yield
    ensure
      Time.testing_offset = 0
    end
  end
end


Then, for any tests that need to simulate time offsets, I require the time_helper:

require File.dirname(__FILE__) + '/../../time_helper'
bjhess posts on Aug 24, 2007 at 06:32
Sorry, details are actually at my blog.
hugocf posts on Aug 24, 2007 at 10:36
Well... I wasn't very happy with the manual "offset" manipulation in my "fix" above,

    def setup
      Time.testing_offset = Time.now - Time.local(2007, 8, 15)  # position all test back in time!
    end


so i've changed the original code a little (maybe a little too much :D) in order to:

* stop exposing the "offset" accessor, create higher-level methods to manipulate it: Time#set(); Time#reset()
* make sure all time jumps are absolute and not affected by any previous time warps
* allow pretend_now_is() to be optionally called without a block, to change the time globally (in fact, the same as calling Time#set() directly)

So here's the fully changed "test/time_helper.rb" (who knows, some might find it useful! :)
There's also some test cases further down...

ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")

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:
  #   class WarpTest < Test::Unit::TestCase
  #   
  #   def setup
  #     pretend_now_is(Time.local(2007, 8, 1))  # position *all* tests back in time!
  #   end
  #   
  #   def teardown
  #     Time.reset    # jump back to the present
  #   end
  #
  #   # If one particular test needs some time jumping of its own...
  #   def test_decades
  #     pretend_now_is(Time.local(1960)) do
  #       assert_equal(1960, Time.now.year)
  #     end
  #     pretend_now_is(Time.local(1970)) do
  #       assert_equal(1970, Time.now.year)
  #     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

module Test   #:nodoc:
  module Unit   #:nodoc:
    class TestCase
      
      # 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
      
    end
  end
end


And the tests "test/units/time_helper_test.rb":

require File.dirname(__FILE__) + '/../test_helper'
require File.dirname(__FILE__) + '/../time_helper'

class TimeHelperTest < Test::Unit::TestCase
  
  def teardown
    Time.reset
  end
  
  def test_time_defaults_to_the_present
    Time.now
    assert_equal(0, Time.offset)
  end
  
  def test_pretend_with_a_block_applies_only_to_the_block
    current_year = Time.now.year
    
    pretend_now_is(Time.local(current_year + 10)) do
      assert_equal(current_year + 10, Time.now.year)
    end
    assert_equal(current_year, Time.now.year)
  end
  
  def test_pretend_without_a_block_changes_time_globally
    current_year = Time.now.year
    assert_equal(current_year, Time.now.year)
    
    pretend_now_is(Time.local(current_year - 10))
    assert_equal(current_year - 10, Time.now.year)
  end
  
  def test_all_time_jumps_are_absolute_and_not_affected_by_any_previous_time_warps
    current_year = Time.now.year
    assert_equal(current_year, Time.now.year)
    
    pretend_now_is(Time.local(current_year - 5))
    assert_equal(current_year - 5, Time.now.year)
    
    pretend_now_is(Time.local(current_year + 5))
    assert_equal(current_year + 5, Time.now.year)
  end
  
  def test_reset_brings_us_back_to_the_preset
    current_year = Time.now.year
    pretend_now_is(Time.local(current_year + 10))
    Time.reset
    
    assert_equal(current_year, Time.now.year)
  end
end



hugocf posts on Aug 30, 2007 at 21:11
Safer sure-fire way to prevent the SystemStackError mentioned above.. It has something to do with getting into a recursive declaration lock, when the Time class is redefined over and over again, and is triggered whenever the helper is included more than once.

By making sure it only extends the Time class once, the error goes away, so I used an “unless” clause wrapping the Time definition in the “time_helper.rb”:

unless Time.respond_to? :real_now # prevent the error: stack level too deep (SystemStackError)
  class Time #:nodoc:
    class <<self
      #... the same stuff as before
    end
  end
end


zaius posts on Oct 06, 2007 at 22:54
hugocf you are my hero! Thanks so much for that code!

You need to create an account or log in to post comments to this site.


Click here to browse all 5141 code snippets

Related Posts