DZone 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

Snippets has posted 5883 posts at DZone. View Full User Profile

Time Warp For Functional And Unit Testing

03.23.2006
| 9180 views |
  • submit to reddit
        
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

Snippets Manager replied on Wed, 2009/09/09 - 12:47pm

Time warp for functional and unit testing system is really good and i was wandering for some this type of code, i love to use it for me as you are helping in quite decent manner, i was here when i was looking for some exclusive packages of reseller web hosting services along with the cheap packages of adult hosting services which are affordable and now i see that many people are going to choose dedicated server colocation services for business and home use for faster and better speed, but i like your work and will return here again.

Snippets Manager replied on Fri, 2007/08/17 - 8:08am

Shoot. Wrong URL: http://github.com/bjhess/time-warp

Snippets Manager replied on Fri, 2007/08/17 - 8:08am

I have used this technique on several projects, so naturally a plugin needed to be created. Here you go! http://github.com/bjhess/time_warp

Snippets Manager replied on Thu, 2007/08/23 - 11:00pm

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 <

Snippets Manager replied on Fri, 2007/08/17 - 8:08am

Sorry, details are actually at my blog.

Snippets Manager replied on Fri, 2007/08/17 - 8:08am

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 < Then, for any tests that need to simulate time offsets, I require the time_helper: require File.dirname(__FILE__) + '/../../time_helper'

Snippets Manager replied on Thu, 2007/08/23 - 11:00pm

I think I might have found a minor bug when you use a global Time.testing_offset different from 0. You need to make sure the pretend_now_is(time) 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 pretend_now_is(time) 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

Snippets Manager replied on Thu, 2007/10/04 - 11:26pm

hugocf you are my hero! Thanks so much for that code!

Snippets Manager replied on Mon, 2006/08/21 - 6:57pm

nice! very helpful. though I needed something to change the date entirely so I modified it a bit: class Time #:nodoc: class <

Snippets Manager replied on Thu, 2007/08/23 - 11:00pm

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) # Test Helper: used only in testing! # # 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 # # (see reference http://snippets.dzone.com/posts/show/1738) class Time class < 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