aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/testing
diff options
context:
space:
mode:
Diffstat (limited to 'activesupport/lib/active_support/testing')
-rw-r--r--activesupport/lib/active_support/testing/assertions.rb199
-rw-r--r--activesupport/lib/active_support/testing/autorun.rb7
-rw-r--r--activesupport/lib/active_support/testing/constant_lookup.rb51
-rw-r--r--activesupport/lib/active_support/testing/declarative.rb28
-rw-r--r--activesupport/lib/active_support/testing/deprecation.rb39
-rw-r--r--activesupport/lib/active_support/testing/file_fixtures.rb36
-rw-r--r--activesupport/lib/active_support/testing/isolation.rb108
-rw-r--r--activesupport/lib/active_support/testing/method_call_assertions.rb43
-rw-r--r--activesupport/lib/active_support/testing/setup_and_teardown.rb52
-rw-r--r--activesupport/lib/active_support/testing/stream.rb44
-rw-r--r--activesupport/lib/active_support/testing/tagged_logging.rb27
-rw-r--r--activesupport/lib/active_support/testing/time_helpers.rb199
12 files changed, 833 insertions, 0 deletions
diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb
new file mode 100644
index 0000000000..f6366bfd39
--- /dev/null
+++ b/activesupport/lib/active_support/testing/assertions.rb
@@ -0,0 +1,199 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ module Testing
+ module Assertions
+ UNTRACKED = Object.new # :nodoc:
+
+ # Asserts that an expression is not truthy. Passes if <tt>object</tt> is
+ # +nil+ or +false+. "Truthy" means "considered true in a conditional"
+ # like <tt>if foo</tt>.
+ #
+ # assert_not nil # => true
+ # assert_not false # => true
+ # assert_not 'foo' # => Expected "foo" to be nil or false
+ #
+ # An error message can be specified.
+ #
+ # assert_not foo, 'foo should be false'
+ def assert_not(object, message = nil)
+ message ||= "Expected #{mu_pp(object)} to be nil or false"
+ assert !object, message
+ end
+
+ # Assertion that the block should not raise an exception.
+ #
+ # Passes if evaluated code in the yielded block raises no exception.
+ #
+ # assert_nothing_raised do
+ # perform_service(param: 'no_exception')
+ # end
+ def assert_nothing_raised
+ yield
+ end
+
+ # Test numeric difference between the return value of an expression as a
+ # result of what is evaluated in the yielded block.
+ #
+ # assert_difference 'Article.count' do
+ # post :create, params: { article: {...} }
+ # end
+ #
+ # An arbitrary expression is passed in and evaluated.
+ #
+ # assert_difference 'Article.last.comments(:reload).size' do
+ # post :create, params: { comment: {...} }
+ # end
+ #
+ # An arbitrary positive or negative difference can be specified.
+ # The default is <tt>1</tt>.
+ #
+ # assert_difference 'Article.count', -1 do
+ # post :delete, params: { id: ... }
+ # end
+ #
+ # An array of expressions can also be passed in and evaluated.
+ #
+ # assert_difference [ 'Article.count', 'Post.count' ], 2 do
+ # post :create, params: { article: {...} }
+ # end
+ #
+ # A lambda or a list of lambdas can be passed in and evaluated:
+ #
+ # assert_difference ->{ Article.count }, 2 do
+ # post :create, params: { article: {...} }
+ # end
+ #
+ # assert_difference [->{ Article.count }, ->{ Post.count }], 2 do
+ # post :create, params: { article: {...} }
+ # end
+ #
+ # An error message can be specified.
+ #
+ # assert_difference 'Article.count', -1, 'An Article should be destroyed' do
+ # post :delete, params: { id: ... }
+ # end
+ def assert_difference(expression, difference = 1, message = nil, &block)
+ expressions = Array(expression)
+
+ exps = expressions.map { |e|
+ e.respond_to?(:call) ? e : lambda { eval(e, block.binding) }
+ }
+ before = exps.map(&:call)
+
+ retval = yield
+
+ expressions.zip(exps).each_with_index do |(code, e), i|
+ error = "#{code.inspect} didn't change by #{difference}"
+ error = "#{message}.\n#{error}" if message
+ assert_equal(before[i] + difference, e.call, error)
+ end
+
+ retval
+ end
+
+ # Assertion that the numeric result of evaluating an expression is not
+ # changed before and after invoking the passed in block.
+ #
+ # assert_no_difference 'Article.count' do
+ # post :create, params: { article: invalid_attributes }
+ # end
+ #
+ # An error message can be specified.
+ #
+ # assert_no_difference 'Article.count', 'An Article should not be created' do
+ # post :create, params: { article: invalid_attributes }
+ # end
+ def assert_no_difference(expression, message = nil, &block)
+ assert_difference expression, 0, message, &block
+ end
+
+ # Assertion that the result of evaluating an expression is changed before
+ # and after invoking the passed in block.
+ #
+ # assert_changes 'Status.all_good?' do
+ # post :create, params: { status: { ok: false } }
+ # end
+ #
+ # You can pass the block as a string to be evaluated in the context of
+ # the block. A lambda can be passed for the block as well.
+ #
+ # assert_changes -> { Status.all_good? } do
+ # post :create, params: { status: { ok: false } }
+ # end
+ #
+ # The assertion is useful to test side effects. The passed block can be
+ # anything that can be converted to string with #to_s.
+ #
+ # assert_changes :@object do
+ # @object = 42
+ # end
+ #
+ # The keyword arguments :from and :to can be given to specify the
+ # expected initial value and the expected value after the block was
+ # executed.
+ #
+ # assert_changes :@object, from: nil, to: :foo do
+ # @object = :foo
+ # end
+ #
+ # An error message can be specified.
+ #
+ # assert_changes -> { Status.all_good? }, 'Expected the status to be bad' do
+ # post :create, params: { status: { incident: true } }
+ # end
+ def assert_changes(expression, message = nil, from: UNTRACKED, to: UNTRACKED, &block)
+ exp = expression.respond_to?(:call) ? expression : -> { eval(expression.to_s, block.binding) }
+
+ before = exp.call
+ retval = yield
+
+ unless from == UNTRACKED
+ error = "#{expression.inspect} isn't #{from.inspect}"
+ error = "#{message}.\n#{error}" if message
+ assert from === before, error
+ end
+
+ after = exp.call
+
+ if to == UNTRACKED
+ error = "#{expression.inspect} didn't changed"
+ error = "#{message}.\n#{error}" if message
+ assert_not_equal before, after, error
+ else
+ error = "#{expression.inspect} didn't change to #{to}"
+ error = "#{message}.\n#{error}" if message
+ assert to === after, error
+ end
+
+ retval
+ end
+
+ # Assertion that the result of evaluating an expression is not changed before
+ # and after invoking the passed in block.
+ #
+ # assert_no_changes 'Status.all_good?' do
+ # post :create, params: { status: { ok: true } }
+ # end
+ #
+ # An error message can be specified.
+ #
+ # assert_no_changes -> { Status.all_good? }, 'Expected the status to be good' do
+ # post :create, params: { status: { ok: false } }
+ # end
+ def assert_no_changes(expression, message = nil, &block)
+ exp = expression.respond_to?(:call) ? expression : -> { eval(expression.to_s, block.binding) }
+
+ before = exp.call
+ retval = yield
+ after = exp.call
+
+ error = "#{expression.inspect} did change to #{after}"
+ error = "#{message}.\n#{error}" if message
+ assert_equal before, after, error
+
+ retval
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/testing/autorun.rb b/activesupport/lib/active_support/testing/autorun.rb
new file mode 100644
index 0000000000..889b41659a
--- /dev/null
+++ b/activesupport/lib/active_support/testing/autorun.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+gem "minitest"
+
+require "minitest"
+
+Minitest.autorun
diff --git a/activesupport/lib/active_support/testing/constant_lookup.rb b/activesupport/lib/active_support/testing/constant_lookup.rb
new file mode 100644
index 0000000000..0fedd486fb
--- /dev/null
+++ b/activesupport/lib/active_support/testing/constant_lookup.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require_relative "../concern"
+require_relative "../inflector"
+
+module ActiveSupport
+ module Testing
+ # Resolves a constant from a minitest spec name.
+ #
+ # Given the following spec-style test:
+ #
+ # describe WidgetsController, :index do
+ # describe "authenticated user" do
+ # describe "returns widgets" do
+ # it "has a controller that exists" do
+ # assert_kind_of WidgetsController, @controller
+ # end
+ # end
+ # end
+ # end
+ #
+ # The test will have the following name:
+ #
+ # "WidgetsController::index::authenticated user::returns widgets"
+ #
+ # The constant WidgetsController can be resolved from the name.
+ # The following code will resolve the constant:
+ #
+ # controller = determine_constant_from_test_name(name) do |constant|
+ # Class === constant && constant < ::ActionController::Metal
+ # end
+ module ConstantLookup
+ extend ::ActiveSupport::Concern
+
+ module ClassMethods # :nodoc:
+ def determine_constant_from_test_name(test_name)
+ names = test_name.split "::"
+ while names.size > 0 do
+ names.last.sub!(/Test$/, "")
+ begin
+ constant = names.join("::").safe_constantize
+ break(constant) if yield(constant)
+ ensure
+ names.pop
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb
new file mode 100644
index 0000000000..7c3403684d
--- /dev/null
+++ b/activesupport/lib/active_support/testing/declarative.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ module Testing
+ module Declarative
+ unless defined?(Spec)
+ # Helper to define a test method using a String. Under the hood, it replaces
+ # spaces with underscores and defines the test method.
+ #
+ # test "verify something" do
+ # ...
+ # end
+ def test(name, &block)
+ test_name = "test_#{name.gsub(/\s+/, '_')}".to_sym
+ defined = method_defined? test_name
+ raise "#{test_name} is already defined in #{self}" if defined
+ if block_given?
+ define_method(test_name, &block)
+ else
+ define_method(test_name) do
+ flunk "No implementation provided for #{name}"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/testing/deprecation.rb b/activesupport/lib/active_support/testing/deprecation.rb
new file mode 100644
index 0000000000..4fda4832cc
--- /dev/null
+++ b/activesupport/lib/active_support/testing/deprecation.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require_relative "../deprecation"
+require_relative "../core_ext/regexp"
+
+module ActiveSupport
+ module Testing
+ module Deprecation #:nodoc:
+ def assert_deprecated(match = nil, deprecator = nil, &block)
+ result, warnings = collect_deprecations(deprecator, &block)
+ assert !warnings.empty?, "Expected a deprecation warning within the block but received none"
+ if match
+ match = Regexp.new(Regexp.escape(match)) unless match.is_a?(Regexp)
+ assert warnings.any? { |w| match.match?(w) }, "No deprecation warning matched #{match}: #{warnings.join(', ')}"
+ end
+ result
+ end
+
+ def assert_not_deprecated(deprecator = nil, &block)
+ result, deprecations = collect_deprecations(deprecator, &block)
+ assert deprecations.empty?, "Expected no deprecation warning within the block but received #{deprecations.size}: \n #{deprecations * "\n "}"
+ result
+ end
+
+ def collect_deprecations(deprecator = nil)
+ deprecator ||= ActiveSupport::Deprecation
+ old_behavior = deprecator.behavior
+ deprecations = []
+ deprecator.behavior = Proc.new do |message, callstack|
+ deprecations << message
+ end
+ result = yield
+ [result, deprecations]
+ ensure
+ deprecator.behavior = old_behavior
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/testing/file_fixtures.rb b/activesupport/lib/active_support/testing/file_fixtures.rb
new file mode 100644
index 0000000000..ad923d1aab
--- /dev/null
+++ b/activesupport/lib/active_support/testing/file_fixtures.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ module Testing
+ # Adds simple access to sample files called file fixtures.
+ # File fixtures are normal files stored in
+ # <tt>ActiveSupport::TestCase.file_fixture_path</tt>.
+ #
+ # File fixtures are represented as +Pathname+ objects.
+ # This makes it easy to extract specific information:
+ #
+ # file_fixture("example.txt").read # get the file's content
+ # file_fixture("example.mp3").size # get the file size
+ module FileFixtures
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :file_fixture_path, instance_writer: false
+ end
+
+ # Returns a +Pathname+ to the fixture file named +fixture_name+.
+ #
+ # Raises +ArgumentError+ if +fixture_name+ can't be found.
+ def file_fixture(fixture_name)
+ path = Pathname.new(File.join(file_fixture_path, fixture_name))
+
+ if path.exist?
+ path
+ else
+ msg = "the directory '%s' does not contain a file named '%s'"
+ raise ArgumentError, msg % [file_fixture_path, fixture_name]
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb
new file mode 100644
index 0000000000..954197a3cc
--- /dev/null
+++ b/activesupport/lib/active_support/testing/isolation.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ module Testing
+ module Isolation
+ require "thread"
+
+ def self.included(klass) #:nodoc:
+ klass.class_eval do
+ parallelize_me!
+ end
+ end
+
+ def self.forking_env?
+ !ENV["NO_FORK"] && Process.respond_to?(:fork)
+ end
+
+ def run
+ serialized = run_in_isolation do
+ super
+ end
+
+ Marshal.load(serialized)
+ end
+
+ module Forking
+ def run_in_isolation(&blk)
+ read, write = IO.pipe
+ read.binmode
+ write.binmode
+
+ pid = fork do
+ read.close
+ yield
+ begin
+ if error?
+ failures.map! { |e|
+ begin
+ Marshal.dump e
+ e
+ rescue TypeError
+ ex = Exception.new e.message
+ ex.set_backtrace e.backtrace
+ Minitest::UnexpectedError.new ex
+ end
+ }
+ end
+ result = Marshal.dump(dup)
+ end
+
+ write.puts [result].pack("m")
+ exit!
+ end
+
+ write.close
+ result = read.read
+ Process.wait2(pid)
+ return result.unpack("m")[0]
+ end
+ end
+
+ module Subprocess
+ ORIG_ARGV = ARGV.dup unless defined?(ORIG_ARGV)
+
+ # Crazy H4X to get this working in windows / jruby with
+ # no forking.
+ def run_in_isolation(&blk)
+ require "tempfile"
+
+ if ENV["ISOLATION_TEST"]
+ yield
+ File.open(ENV["ISOLATION_OUTPUT"], "w") do |file|
+ file.puts [Marshal.dump(dup)].pack("m")
+ end
+ exit!
+ else
+ Tempfile.open("isolation") do |tmpfile|
+ env = {
+ "ISOLATION_TEST" => self.class.name,
+ "ISOLATION_OUTPUT" => tmpfile.path
+ }
+
+ test_opts = "-n#{self.class.name}##{name}"
+
+ load_path_args = []
+ $-I.each do |p|
+ load_path_args << "-I"
+ load_path_args << File.expand_path(p)
+ end
+
+ child = IO.popen([env, Gem.ruby, *load_path_args, $0, *ORIG_ARGV, test_opts])
+
+ begin
+ Process.wait(child.pid)
+ rescue Errno::ECHILD # The child process may exit before we wait
+ nil
+ end
+
+ return tmpfile.read.unpack("m")[0]
+ end
+ end
+ end
+ end
+
+ include forking_env? ? Forking : Subprocess
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/testing/method_call_assertions.rb b/activesupport/lib/active_support/testing/method_call_assertions.rb
new file mode 100644
index 0000000000..c6358002ea
--- /dev/null
+++ b/activesupport/lib/active_support/testing/method_call_assertions.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require "minitest/mock"
+
+module ActiveSupport
+ module Testing
+ module MethodCallAssertions # :nodoc:
+ private
+ def assert_called(object, method_name, message = nil, times: 1, returns: nil)
+ times_called = 0
+
+ object.stub(method_name, proc { times_called += 1; returns }) { yield }
+
+ error = "Expected #{method_name} to be called #{times} times, " \
+ "but was called #{times_called} times"
+ error = "#{message}.\n#{error}" if message
+ assert_equal times, times_called, error
+ end
+
+ def assert_called_with(object, method_name, args = [], returns: nil)
+ mock = Minitest::Mock.new
+
+ if args.all? { |arg| arg.is_a?(Array) }
+ args.each { |arg| mock.expect(:call, returns, arg) }
+ else
+ mock.expect(:call, returns, args)
+ end
+
+ object.stub(method_name, mock) { yield }
+
+ mock.verify
+ end
+
+ def assert_not_called(object, method_name, message = nil, &block)
+ assert_called(object, method_name, message, times: 0, &block)
+ end
+
+ def stub_any_instance(klass, instance: klass.new)
+ klass.stub(:new, instance) { yield instance }
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb
new file mode 100644
index 0000000000..18de7185d9
--- /dev/null
+++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require_relative "../concern"
+require_relative "../callbacks"
+
+module ActiveSupport
+ module Testing
+ # Adds support for +setup+ and +teardown+ callbacks.
+ # These callbacks serve as a replacement to overwriting the
+ # <tt>#setup</tt> and <tt>#teardown</tt> methods of your TestCase.
+ #
+ # class ExampleTest < ActiveSupport::TestCase
+ # setup do
+ # # ...
+ # end
+ #
+ # teardown do
+ # # ...
+ # end
+ # end
+ module SetupAndTeardown
+ extend ActiveSupport::Concern
+
+ included do
+ include ActiveSupport::Callbacks
+ define_callbacks :setup, :teardown
+ end
+
+ module ClassMethods
+ # Add a callback, which runs before <tt>TestCase#setup</tt>.
+ def setup(*args, &block)
+ set_callback(:setup, :before, *args, &block)
+ end
+
+ # Add a callback, which runs after <tt>TestCase#teardown</tt>.
+ def teardown(*args, &block)
+ set_callback(:teardown, :after, *args, &block)
+ end
+ end
+
+ def before_setup # :nodoc:
+ super
+ run_callbacks :setup
+ end
+
+ def after_teardown # :nodoc:
+ run_callbacks :teardown
+ super
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/testing/stream.rb b/activesupport/lib/active_support/testing/stream.rb
new file mode 100644
index 0000000000..d070a1793d
--- /dev/null
+++ b/activesupport/lib/active_support/testing/stream.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ module Testing
+ module Stream #:nodoc:
+ private
+
+ def silence_stream(stream)
+ old_stream = stream.dup
+ stream.reopen(IO::NULL)
+ stream.sync = true
+ yield
+ ensure
+ stream.reopen(old_stream)
+ old_stream.close
+ end
+
+ def quietly
+ silence_stream(STDOUT) do
+ silence_stream(STDERR) do
+ yield
+ end
+ end
+ end
+
+ def capture(stream)
+ stream = stream.to_s
+ captured_stream = Tempfile.new(stream)
+ stream_io = eval("$#{stream}")
+ origin_stream = stream_io.dup
+ stream_io.reopen(captured_stream)
+
+ yield
+
+ stream_io.rewind
+ return captured_stream.read
+ ensure
+ captured_stream.close
+ captured_stream.unlink
+ stream_io.reopen(origin_stream)
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/testing/tagged_logging.rb b/activesupport/lib/active_support/testing/tagged_logging.rb
new file mode 100644
index 0000000000..9ca50c7918
--- /dev/null
+++ b/activesupport/lib/active_support/testing/tagged_logging.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ module Testing
+ # Logs a "PostsControllerTest: test name" heading before each test to
+ # make test.log easier to search and follow along with.
+ module TaggedLogging #:nodoc:
+ attr_writer :tagged_logger
+
+ def before_setup
+ if tagged_logger && tagged_logger.info?
+ heading = "#{self.class}: #{name}"
+ divider = "-" * heading.size
+ tagged_logger.info divider
+ tagged_logger.info heading
+ tagged_logger.info divider
+ end
+ super
+ end
+
+ private
+ def tagged_logger
+ @tagged_logger ||= (defined?(Rails.logger) && Rails.logger)
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb
new file mode 100644
index 0000000000..fa5f46736c
--- /dev/null
+++ b/activesupport/lib/active_support/testing/time_helpers.rb
@@ -0,0 +1,199 @@
+# frozen_string_literal: true
+
+require_relative "../core_ext/string/strip" # for strip_heredoc
+require_relative "../core_ext/time/calculations"
+require "concurrent/map"
+
+module ActiveSupport
+ module Testing
+ class SimpleStubs # :nodoc:
+ Stub = Struct.new(:object, :method_name, :original_method)
+
+ def initialize
+ @stubs = Concurrent::Map.new { |h, k| h[k] = {} }
+ end
+
+ def stub_object(object, method_name, &block)
+ if stub = stubbing(object, method_name)
+ unstub_object(stub)
+ end
+
+ new_name = "__simple_stub__#{method_name}"
+
+ @stubs[object.object_id][method_name] = Stub.new(object, method_name, new_name)
+
+ object.singleton_class.send :alias_method, new_name, method_name
+ object.define_singleton_method(method_name, &block)
+ end
+
+ def unstub_all!
+ @stubs.each_value do |object_stubs|
+ object_stubs.each_value do |stub|
+ unstub_object(stub)
+ end
+ end
+ @stubs.clear
+ end
+
+ def stubbing(object, method_name)
+ @stubs[object.object_id][method_name]
+ end
+
+ private
+
+ def unstub_object(stub)
+ singleton_class = stub.object.singleton_class
+ singleton_class.send :undef_method, stub.method_name
+ singleton_class.send :alias_method, stub.method_name, stub.original_method
+ singleton_class.send :undef_method, stub.original_method
+ end
+ end
+
+ # Contains helpers that help you test passage of time.
+ module TimeHelpers
+ def after_teardown
+ travel_back
+ super
+ end
+
+ # Changes current time to the time in the future or in the past by a given time difference by
+ # stubbing +Time.now+, +Date.today+, and +DateTime.now+. The stubs are automatically removed
+ # at the end of the test.
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel 1.day
+ # Time.current # => Sun, 10 Nov 2013 15:34:49 EST -05:00
+ # Date.current # => Sun, 10 Nov 2013
+ # DateTime.current # => Sun, 10 Nov 2013 15:34:49 -0500
+ #
+ # This method also accepts a block, which will return the current time back to its original
+ # state at the end of the block:
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel 1.day do
+ # User.create.created_at # => Sun, 10 Nov 2013 15:34:49 EST -05:00
+ # end
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ def travel(duration, &block)
+ travel_to Time.now + duration, &block
+ end
+
+ # Changes current time to the given time by stubbing +Time.now+,
+ # +Date.today+, and +DateTime.now+ to return the time or date passed into this method.
+ # The stubs are automatically removed at the end of the test.
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel_to Time.zone.local(2004, 11, 24, 01, 04, 44)
+ # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
+ # Date.current # => Wed, 24 Nov 2004
+ # DateTime.current # => Wed, 24 Nov 2004 01:04:44 -0500
+ #
+ # Dates are taken as their timestamp at the beginning of the day in the
+ # application time zone. <tt>Time.current</tt> returns said timestamp,
+ # and <tt>Time.now</tt> its equivalent in the system time zone. Similarly,
+ # <tt>Date.current</tt> returns a date equal to the argument, and
+ # <tt>Date.today</tt> the date according to <tt>Time.now</tt>, which may
+ # be different. (Note that you rarely want to deal with <tt>Time.now</tt>,
+ # or <tt>Date.today</tt>, in order to honor the application time zone
+ # please always use <tt>Time.current</tt> and <tt>Date.current</tt>.)
+ #
+ # Note that the usec for the time passed will be set to 0 to prevent rounding
+ # errors with external services, like MySQL (which will round instead of floor,
+ # leading to off-by-one-second errors).
+ #
+ # This method also accepts a block, which will return the current time back to its original
+ # state at the end of the block:
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel_to Time.zone.local(2004, 11, 24, 01, 04, 44) do
+ # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
+ # end
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ def travel_to(date_or_time)
+ if block_given? && simple_stubs.stubbing(Time, :now)
+ travel_to_nested_block_call = <<-MSG.strip_heredoc
+
+ Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing.
+
+ Instead of:
+
+ travel_to 2.days.from_now do
+ # 2 days from today
+ travel_to 3.days.from_now do
+ # 5 days from today
+ end
+ end
+
+ preferred way to achieve above is:
+
+ travel 2.days do
+ # 2 days from today
+ end
+
+ travel 5.days do
+ # 5 days from today
+ end
+
+ MSG
+ raise travel_to_nested_block_call
+ end
+
+ if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime)
+ now = date_or_time.midnight.to_time
+ else
+ now = date_or_time.to_time.change(usec: 0)
+ end
+
+ simple_stubs.stub_object(Time, :now) { at(now.to_i) }
+ simple_stubs.stub_object(Date, :today) { jd(now.to_date.jd) }
+ simple_stubs.stub_object(DateTime, :now) { jd(now.to_date.jd, now.hour, now.min, now.sec, Rational(now.utc_offset, 86400)) }
+
+ if block_given?
+ begin
+ yield
+ ensure
+ travel_back
+ end
+ end
+ end
+
+ # Returns the current time back to its original state, by removing the stubs added by
+ # +travel+ and +travel_to+.
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel_to Time.zone.local(2004, 11, 24, 01, 04, 44)
+ # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
+ # travel_back
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ def travel_back
+ simple_stubs.unstub_all!
+ end
+
+ # Calls +travel_to+ with +Time.now+.
+ #
+ # Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
+ # freeze_time
+ # sleep(1)
+ # Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
+ #
+ # This method also accepts a block, which will return the current time back to its original
+ # state at the end of the block:
+ #
+ # Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
+ # freeze_time do
+ # sleep(1)
+ # User.create.created_at # => Sun, 09 Jul 2017 15:34:49 EST -05:00
+ # end
+ # Time.current # => Sun, 09 Jul 2017 15:34:50 EST -05:00
+ def freeze_time(&block)
+ travel_to Time.now, &block
+ end
+
+ private
+
+ def simple_stubs
+ @simple_stubs ||= SimpleStubs.new
+ end
+ end
+ end
+end