aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport
diff options
context:
space:
mode:
authorJeremy Kemper <jeremy@bitsweat.net>2006-11-02 09:59:52 +0000
committerJeremy Kemper <jeremy@bitsweat.net>2006-11-02 09:59:52 +0000
commit7ca2b657f01b09014cd1fb0494fb13ab63dafe5c (patch)
tree986d9064b609d319509452ba40064e9e5480ce1a /activesupport
parentac715b2bcbc1dee525339edec86362261e017bbe (diff)
downloadrails-7ca2b657f01b09014cd1fb0494fb13ab63dafe5c.tar.gz
rails-7ca2b657f01b09014cd1fb0494fb13ab63dafe5c.tar.bz2
rails-7ca2b657f01b09014cd1fb0494fb13ab63dafe5c.zip
Update bundled flexmock to latest 0.4.3.
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@5403 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'activesupport')
-rw-r--r--activesupport/lib/active_support/vendor/flexmock.rb1052
1 files changed, 1018 insertions, 34 deletions
diff --git a/activesupport/lib/active_support/vendor/flexmock.rb b/activesupport/lib/active_support/vendor/flexmock.rb
index 59d2f894cc..ad6877715a 100644
--- a/activesupport/lib/active_support/vendor/flexmock.rb
+++ b/activesupport/lib/active_support/vendor/flexmock.rb
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
#---
-# Copyright 2003, 2004 by Jim Weirich (jim@weriichhouse.org).
+# Copyright 2003, 2004, 2005, 2006 by Jim Weirich (jim@weirichhouse.org).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
@@ -11,21 +11,59 @@
require 'test/unit'
+######################################################################
# FlexMock is a flexible mock object suitable for using with Ruby's
# Test::Unit unit test framework. FlexMock has a simple interface
# that's easy to remember, and leaves the hard stuff to all those
# other mock object implementations.
#
-# Usage: See TestSamples for example usage.
-
+# Basic Usage:
+#
+# m = FlexMock.new("name")
+# m.mock_handle(:meth) { |args| assert_stuff }
+#
+# Simplified Usage:
+#
+# m = FlexMock.new("name")
+# m.should_receive(:upcase).with("stuff").
+# returns("STUFF")
+# m.should_receive(:downcase).with(String).
+# returns { |s| s.downcase }.once
+#
+# With Test::Unit Integration:
+#
+# class TestSomething < Test::Unit::TestCase
+# include FlexMock::TestCase
+#
+# def test_something
+# m = flexmock("name")
+# m.should_receive(:hi).and_return("Hello")
+# m.hi
+# end
+# end
+#
+# Note: When using Test::Unit integeration, don't forget to include
+# FlexMock::TestCase. Also, if you override +teardown+, make sure you
+# call +super+.
+#
class FlexMock
include Test::Unit::Assertions
- # Create a FlexMock object.
- def initialize
- @handlers = Hash.new
- @counts = Hash.new(0)
- @expected_counts = Hash.new
+ class BadInterceptionError < RuntimeError; end
+
+ attr_reader :mock_name, :mock_groups
+ attr_accessor :mock_current_order
+
+ # Create a FlexMock object with the given name. The name is used in
+ # error messages.
+ def initialize(name="unknown")
+ @mock_name = name
+ @expectations = Hash.new
+ @allocated_order = 0
+ @mock_current_order = 0
+ @mock_groups = {}
+ @ignore_missing = false
+ @verified = false
end
# Handle all messages denoted by +sym+ by calling the given block
@@ -34,51 +72,997 @@ class FlexMock
# that by passing in the number of expected calls as a second
# paramter.
def mock_handle(sym, expected_count=nil, &block)
- if block_given?
- @handlers[sym] = block
- else
- @handlers[sym] = proc { }
- end
- @expected_counts[sym] = expected_count if expected_count
+ self.should_receive(sym).times(expected_count).returns(&block)
end
# Verify that each method that had an explicit expected count was
# actually called that many times.
def mock_verify
- @expected_counts.keys.each do |key|
- assert_equal @expected_counts[key], @counts[key],
- "Expected method #{key} to be called #{@expected_counts[key]} times, " +
- "got #{@counts[key]}"
+ return if @verified
+ @verified = true
+ mock_wrap do
+ @expectations.each do |sym, handler|
+ handler.mock_verify
+ end
end
end
- # Report how many times a method was called.
- def mock_count(sym)
- @counts[sym]
+ # Teardown and infrastructure setup for this mock.
+ def mock_teardown
+ end
+
+ # Allocation a new order number from the mock.
+ def mock_allocate_order
+ @auto_allocate = true
+ @allocated_order += 1
end
# Ignore all undefined (missing) method calls.
- def mock_ignore_missing
+ def should_ignore_missing
@ignore_missing = true
end
+ alias mock_ignore_missing should_ignore_missing
# Handle missing methods by attempting to look up a handler.
def method_missing(sym, *args, &block)
- if handler = @handlers[sym]
- @counts[sym] += 1
- args << block if block_given?
- handler.call(*args)
+ mock_wrap do
+ if handler = @expectations[sym]
+ args << block if block_given?
+ handler.call(*args)
+ else
+ super(sym, *args, &block) unless @ignore_missing
+ end
+ end
+ end
+
+ # Save the original definition of respond_to? for use a bit later.
+ alias mock_respond_to? respond_to?
+
+ # Override the built-in respond_to? to include the mocked methods.
+ def respond_to?(sym)
+ super || (@expectations[sym] ? true : @ignore_missing)
+ end
+
+ # Override the built-in +method+ to include the mocked methods.
+ def method(sym)
+ @expectations[sym] || super
+ rescue NameError => ex
+ if @ignore_missing
+ proc { }
else
- super(sym, *args, &block) unless @ignore_missing
+ raise ex
+ end
+ end
+
+ # Declare that the mock object should receive a message with the
+ # given name. An expectation object for the method name is returned
+ # as the result of this method. Further expectation constraints can
+ # be added by chaining to the result.
+ #
+ # See Expectation for a list of declarators that can be used.
+ def should_receive(sym)
+ @expectations[sym] ||= ExpectationDirector.new(sym)
+ result = Expectation.new(self, sym)
+ @expectations[sym] << result
+ override_existing_method(sym) if mock_respond_to?(sym)
+ result
+ end
+
+ # Override the existing definition of method +sym+ in the mock.
+ # Most methods depend on the method_missing trick to be invoked.
+ # However, if the method already exists, it will not call
+ # method_missing. This method defines a singleton method on the
+ # mock to explicitly invoke the method_missing logic.
+ def override_existing_method(sym)
+ sclass.class_eval "def #{sym}(*args, &block) method_missing(:#{sym}, *args, &block) end"
+ end
+ private :override_existing_method
+
+ # Return the singleton class of the mock object.
+ def sclass
+ class << self; self; end
+ end
+ private :sclass
+
+ # Declare that the mock object should expect methods by providing a
+ # recorder for the methods and having the user invoke the expected
+ # methods in a block. Further expectations may be applied the
+ # result of the recording call.
+ #
+ # Example Usage:
+ #
+ # mock.should_expect do |record|
+ # record.add(Integer, 4) { |a, b|
+ # a + b
+ # }.at_least.once
+ #
+ def should_expect
+ yield Recorder.new(self)
+ end
+
+ # Return a factory object that returns this mock. This is useful in
+ # Class Interception.
+ def mock_factory
+ Factory.new(self)
+ end
+
+ class << self
+ include Test::Unit::Assertions
+
+ # Class method to make sure that verify is called at the end of a
+ # test. One mock object will be created for each name given to
+ # the use method. The mocks will be passed to the block as
+ # arguments. If no names are given, then a single anonymous mock
+ # object will be created.
+ #
+ # At the end of the use block, each mock object will be verified
+ # to make sure the proper number of calls have been made.
+ #
+ # Usage:
+ #
+ # FlexMock.use("name") do |mock| # Creates a mock named "name"
+ # mock.should_receive(:meth).
+ # returns(0).once
+ # end # mock is verified here
+ #
+ # NOTE: If you include FlexMock::TestCase into your test case
+ # file, you can create mocks that will be automatically verified in
+ # the test teardown by using the +flexmock+ method.
+ #
+ def use(*names)
+ names = ["unknown"] if names.empty?
+ got_excecption = false
+ mocks = names.collect { |n| new(n) }
+ yield(*mocks)
+ rescue Exception => ex
+ got_exception = true
+ raise
+ ensure
+ mocks.each do |mock|
+ mock.mock_verify unless got_exception
+ end
+ end
+
+ # Class method to format a method name and argument list as a nice
+ # looking string.
+ def format_args(sym, args)
+ if args
+ "#{sym}(#{args.collect { |a| a.inspect }.join(', ')})"
+ else
+ "#{sym}(*args)"
+ end
+ end
+
+ # Check will assert the block returns true. If it doesn't, an
+ # assertion failure is triggered with the given message.
+ def check(msg, &block)
+ assert_block(msg, &block)
+ end
+ end
+
+ private
+
+ # Wrap a block of code so the any assertion errors are wrapped so
+ # that the mock name is added to the error message .
+ def mock_wrap(&block)
+ yield
+ rescue Test::Unit::AssertionFailedError => ex
+ raise Test::Unit::AssertionFailedError,
+ "in mock '#{@mock_name}': #{ex.message}",
+ ex.backtrace
+ end
+
+ ####################################################################
+ # A Factory object is returned from a mock_factory method call. The
+ # factory merely returns the manufactured object it is initialized
+ # with. The factory is handy to use with class interception,
+ # allowing the intercepted class to return the mock object.
+ #
+ # If the user needs more control over the mock factory, they are
+ # free to create their own.
+ #
+ # Typical Usage:
+ # intercept(Bar).in(Foo).with(a_mock.mack_factory)
+ #
+ class Factory
+ def initialize(manufactured_object)
+ @obj = manufactured_object
+ end
+ def new(*args, &block)
+ @obj
+ end
+ end
+
+ ####################################################################
+ # The expectation director is responsible for routing calls to the
+ # correct expectations for a given argument list.
+ #
+ class ExpectationDirector
+
+ # Create an ExpectationDirector for a mock object.
+ def initialize(sym)
+ @sym = sym
+ @expectations = []
+ @expected_order = nil
+ end
+
+ # Invoke the expectations for a given set of arguments.
+ #
+ # First, look for an expectation that matches the arguements and
+ # is eligible to be called. Failing that, look for a expectation
+ # that matches the arguments (at this point it will be ineligible,
+ # but at least we will get a good failure message). Finally,
+ # check for expectations that don't have any argument matching
+ # criteria.
+ def call(*args)
+ exp = @expectations.find { |e| e.match_args(args) && e.eligible? } ||
+ @expectations.find { |e| e.match_args(args) } ||
+ @expectations.find { |e| e.expected_args.nil? }
+ FlexMock.check("no matching handler found for " +
+ FlexMock.format_args(@sym, args)) { ! exp.nil? }
+ exp.verify_call(*args)
+ end
+
+ # Same as call.
+ def [](*args)
+ call(*args)
+ end
+
+ # Append an expectation to this director.
+ def <<(expectation)
+ @expectations << expectation
+ end
+
+ # Do the post test verification for this directory. Check all the
+ # expectations.
+ def mock_verify
+ @expectations.each do |exp|
+ exp.mock_verify
+ end
+ end
+ end
+
+ ####################################################################
+ # Match any object
+ class AnyMatcher
+ def ===(target)
+ true
+ end
+ def inspect
+ "ANY"
+ end
+ end
+
+ ####################################################################
+ # Match only things that are equal.
+ class EqualMatcher
+ def initialize(obj)
+ @obj = obj
+ end
+ def ===(target)
+ @obj == target
+ end
+ def inspect
+ "==(#{@obj.inspect})"
+ end
+ end
+
+ ANY = AnyMatcher.new
+
+ ####################################################################
+ # Match only things where the block evaluates to true.
+ class ProcMatcher
+ def initialize(&block)
+ @block = block
+ end
+ def ===(target)
+ @block.call(target)
+ end
+ def inspect
+ "on{...}"
+ end
+ end
+
+ ####################################################################
+ # Include this module in your test class if you wish to use the +eq+
+ # and +any+ argument matching methods without a prefix. (Otherwise
+ # use <tt>FlexMock.any</tt> and <tt>FlexMock.eq(obj)</tt>.
+ #
+ module ArgumentTypes
+ # Return an argument matcher that matches any argument.
+ def any
+ ANY
+ end
+
+ # Return an argument matcher that only matches things equal to
+ # (==) the given object.
+ def eq(obj)
+ EqualMatcher.new(obj)
+ end
+
+ # Return an argument matcher that matches any object, that when
+ # passed to the supplied block, will cause the block to return
+ # true.
+ def on(&block)
+ ProcMatcher.new(&block)
end
end
+ extend ArgumentTypes
+
+ ####################################################################
+ # Base class for all the count validators.
+ #
+ class CountValidator
+ include Test::Unit::Assertions
+ def initialize(expectation, limit)
+ @exp = expectation
+ @limit = limit
+ end
+
+ # If the expectation has been called +n+ times, is it still
+ # eligible to be called again? The default answer compares n to
+ # the established limit.
+ def eligible?(n)
+ n < @limit
+ end
+ end
+
+ ####################################################################
+ # Validator for exact call counts.
+ #
+ class ExactCountValidator < CountValidator
+ # Validate that the method expectation was called exactly +n+
+ # times.
+ def validate(n)
+ assert_equal @limit, n,
+ "method '#{@exp}' called incorrect number of times"
+ end
+ end
+
+ ####################################################################
+ # Validator for call counts greater than or equal to a limit.
+ #
+ class AtLeastCountValidator < CountValidator
+ # Validate the method expectation was called no more than +n+
+ # times.
+ def validate(n)
+ assert n >= @limit,
+ "Method '#{@exp}' should be called at least #{@limit} times,\n" +
+ "only called #{n} times"
+ end
+
+ # If the expectation has been called +n+ times, is it still
+ # eligible to be called again? Since this validator only
+ # establishes a lower limit, not an upper limit, then the answer
+ # is always true.
+ def eligible?(n)
+ true
+ end
+ end
+
+ ####################################################################
+ # Validator for call counts less than or equal to a limit.
+ #
+ class AtMostCountValidator < CountValidator
+ # Validate the method expectation was called at least +n+ times.
+ def validate(n)
+ assert n <= @limit,
+ "Method '#{@exp}' should be called at most #{@limit} times,\n" +
+ "only called #{n} times"
+ end
+ end
+
+ ####################################################################
+ # An Expectation is returned from each +should_receive+ message sent
+ # to mock object. Each expectation records how a message matching
+ # the message name (argument to +should_receive+) and the argument
+ # list (given by +with+) should behave. Mock expectations can be
+ # recorded by chaining the declaration methods defined in this
+ # class.
+ #
+ # For example:
+ #
+ # mock.should_receive(:meth).with(args).and_returns(result)
+ #
+ class Expectation
+ include Test::Unit::Assertions
+
+ attr_reader :expected_args, :mock, :order_number
+
+ # Create an expectation for a method named +sym+.
+ def initialize(mock, sym)
+ @mock = mock
+ @sym = sym
+ @expected_args = nil
+ @count_validators = []
+ @count_validator_class = ExactCountValidator
+ @actual_count = 0
+ @return_value = nil
+ @return_block = lambda { @return_value }
+ @order_number = nil
+ end
+
+ def to_s
+ FlexMock.format_args(@sym, @expected_args)
+ end
- # Class method to make sure that verify is called at the end of a
- # test.
- def self.use
- mock = new
- yield mock
- ensure
- mock.mock_verify
+ # Verify the current call with the given arguments matches the
+ # expectations recorded in this object.
+ def verify_call(*args)
+ validate_order
+ @actual_count += 1
+ @return_block.call(*args)
+ end
+
+ # Is this expectation eligible to be called again? It is eligible
+ # only if all of its count validators agree that it is eligible.
+ def eligible?
+ @count_validators.all? { |v| v.eligible?(@actual_count) }
+ end
+
+ # Validate that the order
+ def validate_order
+ return if @order_number.nil?
+ FlexMock.check("method #{to_s} called out of order " +
+ "(expected order #{@order_number}, was #{@mock.mock_current_order})") {
+ @order_number >= @mock.mock_current_order
+ }
+ @mock.mock_current_order = @order_number
+ end
+ private :validate_order
+
+ # Validate the correct number of calls have been made. Called by
+ # the teardown process.
+ def mock_verify
+ @count_validators.each do |v|
+ v.validate(@actual_count)
+ end
+ end
+
+ # Does the argument list match this expectation's argument
+ # specification.
+ def match_args(args)
+ return false if @expected_args.nil?
+ return false if args.size != @expected_args.size
+ (0...args.size).all? { |i| match_arg(@expected_args[i], args[i]) }
+ end
+
+ # Does the expected argument match the corresponding actual value.
+ def match_arg(expected, actual)
+ expected === actual ||
+ expected == actual ||
+ ( Regexp === expected && expected === actual.to_s )
+ end
+
+ # Declare that the method should expect the given argument list.
+ def with(*args)
+ @expected_args = args
+ self
+ end
+
+ # Declare that the method should be called with no arguments.
+ def with_no_args
+ with
+ end
+
+ # Declare that the method can be called with any number of
+ # arguments of any type.
+ def with_any_args
+ @expected_args = nil
+ self
+ end
+
+ # Declare that the method returns a particular value (when the
+ # argument list is matched).
+ #
+ # * If a single value is given, it will be returned for all matching
+ # calls.
+ # * If multiple values are given, each value will be returned in turn for
+ # each successive call. If the number of matching calls is greater
+ # than the number of values, the last value will be returned for
+ # the extra matching calls.
+ # * If a block is given, it is evaluated on each call and its
+ # value is returned.
+ #
+ # For example:
+ #
+ # mock.should_receive(:f).returns(12) # returns 12
+ #
+ # mock.should_receive(:f).with(String). # returns an
+ # returns { |str| str.upcase } # upcased string
+ #
+ # +and_return+ is an alias for +returns+.
+ #
+ def returns(*args, &block)
+ @return_block = block_given? ?
+ block :
+ lambda { args.size == 1 ? args.first : args.shift }
+ self
+ end
+ alias :and_return :returns # :nodoc:
+
+ # Declare that the method may be called any number of times.
+ def zero_or_more_times
+ at_least.never
+ end
+
+ # Declare that the method is called +limit+ times with the
+ # declared argument list. This may be modified by the +at_least+
+ # and +at_most+ declarators.
+ def times(limit)
+ @count_validators << @count_validator_class.new(self, limit) unless limit.nil?
+ @count_validator_class = ExactCountValidator
+ self
+ end
+
+ # Declare that the method is never expected to be called with the
+ # given argument list. This may be modified by the +at_least+ and
+ # +at_most+ declarators.
+ def never
+ times(0)
+ end
+
+ # Declare that the method is expected to be called exactly once
+ # with the given argument list. This may be modified by the
+ # +at_least+ and +at_most+ declarators.
+ def once
+ times(1)
+ end
+
+ # Declare that the method is expected to be called exactly twice
+ # with the given argument list. This may be modified by the
+ # +at_least+ and +at_most+ declarators.
+ def twice
+ times(2)
+ end
+
+ # Modifies the next call count declarator (+times+, +never+,
+ # +once+ or +twice+) so that the declarator means the method is
+ # called at least that many times.
+ #
+ # E.g. method f must be called at least twice:
+ #
+ # mock.should_receive(:f).at_least.twice
+ #
+ def at_least
+ @count_validator_class = AtLeastCountValidator
+ self
+ end
+
+ # Modifies the next call count declarator (+times+, +never+,
+ # +once+ or +twice+) so that the declarator means the method is
+ # called at most that many times.
+ #
+ # E.g. method f must be called no more than twice
+ #
+ # mock.should_receive(:f).at_most.twice
+ #
+ def at_most
+ @count_validator_class = AtMostCountValidator
+ self
+ end
+
+ # Declare that the given method must be called in order. All
+ # ordered method calls must be received in the order specified by
+ # the ordering of the +should_receive+ messages. Receiving a
+ # methods out of the specified order will cause a test failure.
+ #
+ # If the user needs more fine control over ordering
+ # (e.g. specifying that a group of messages may be received in any
+ # order as long as they all come after another group of messages),
+ # a _group_ _name_ may be specified in the +ordered+ calls. All
+ # messages within the same group may be received in any order.
+ #
+ # For example, in the following, messages +flip+ and +flop+ may be
+ # received in any order (because they are in the same group), but
+ # must occur strictly after +start+ but before +end+. The message
+ # +any_time+ may be received at any time because it is not
+ # ordered.
+ #
+ # m = FlexMock.new
+ # m.should_receive(:any_time)
+ # m.should_receive(:start).ordered
+ # m.should_receive(:flip).ordered(:flip_flop_group)
+ # m.should_receive(:flop).ordered(:flip_flop_group)
+ # m.should_receive(:end).ordered
+ #
+ def ordered(group_name=nil)
+ if group_name.nil?
+ @order_number = @mock.mock_allocate_order
+ elsif (num = @mock.mock_groups[group_name])
+ @order_number = num
+ else
+ @order_number = @mock.mock_allocate_order
+ @mock.mock_groups[group_name] = @order_number
+ end
+ self
+ end
+ end
+
+ ####################################################################
+ # Translate arbitrary method calls into expectations on the given
+ # mock object.
+ #
+ class Recorder
+ include FlexMock::ArgumentTypes
+
+ # Create a method recorder for the mock +mock+.
+ def initialize(mock)
+ @mock = mock
+ @strict = false
+ end
+
+ # Place the record in strict mode. While recording expectations
+ # in strict mode, the following will be true.
+ #
+ # * All expectations will be expected in the order they were
+ # recorded.
+ # * All expectations will be expected once.
+ # * All arguments will be placed in exact match mode,
+ # including regular expressions and class objects.
+ #
+ # Strict mode is usually used when giving the recorder to a known
+ # good algorithm. Strict mode captures the exact sequence of
+ # calls and validate that the code under test performs the exact
+ # same sequence of calls.
+ #
+ # The recorder may exit strict mode via a
+ # <tt>should_be_strict(false)</tt> call. Non-strict expectations
+ # may be recorded at that point, or even explicit expectations
+ # (using +should_receieve+) can be specified.
+ #
+ def should_be_strict(is_strict=true)
+ @strict = is_strict
+ end
+
+ # Is the recorder in strict mode?
+ def strict?
+ @strict
+ end
+
+ # Record an expectation for receiving the method +sym+ with the
+ # given arguments.
+ def method_missing(sym, *args, &block)
+ expectation = @mock.should_receive(sym).and_return(&block)
+ if @strict
+ args = args.collect { |arg| eq(arg) }
+ expectation.with(*args).ordered.once
+ else
+ expectation.with(*args)
+ end
+ expectation
+ end
+ end
+
+ ####################################################################
+ # Test::Unit::TestCase Integration.
+ #
+ # Include this module in any TestCase class in a Test::Unit test
+ # suite to get integration with FlexMock. When this module is
+ # included, mocks may be created with a simple call to the
+ # +flexmock+ method. Mocks created with via the method call will
+ # automatically be verified in the teardown of the test case.
+ #
+ # <b>Note:</b> If you define a +teardown+ method in the test case,
+ # <em>dont' forget to invoke the +super+ method!</em> Failure to
+ # invoke super will cause all mocks to not be verified.
+ #
+ module TestCase
+ include ArgumentTypes
+
+ # Teardown the test case, verifying any mocks that might have been
+ # defined in this test case.
+ def teardown
+ super
+ flexmock_teardown
+ end
+
+ # Do the flexmock specific teardown stuff.
+ def flexmock_teardown
+ @flexmock_created_mocks ||= []
+ if passed?
+ @flexmock_created_mocks.each do |m|
+ m.mock_verify
+ end
+ end
+ ensure
+ @flexmock_created_mocks.each do |m|
+ m.mock_teardown
+ end
+ @flexmock_interceptors ||= []
+ @flexmock_interceptors.each do |i|
+ i.restore
+ end
+ end
+
+ # Create a FlexMock object with the given name. Mocks created
+ # with this method will be automatically verify during teardown
+ # (assuming the the flexmock teardown isn't overridden).
+ #
+ # If a block is given, then the mock object is passed to the block and
+ # may be configured in the block.
+ def flexmock(name="unknown")
+ mock = FlexMock.new(name)
+ yield(mock) if block_given?
+ flexmock_remember(mock)
+ mock
+ end
+
+ # Stub the given object by overriding the behavior of individual methods.
+ # The stub object returned will respond to the +should_receive+
+ # method, just like normal stubs. Singleton methods cannot be
+ # stubbed.
+ #
+ # Example: Stub out DBI to return a fake db connection.
+ #
+ # flexstub(DBI).should_receive(:connect).and_return {
+ # fake_db = flexmock("db connection")
+ # fake_db.should_receive(:select_all).and_return(...)
+ # fake_db
+ # }
+ #
+ def flexstub(obj, name=nil)
+ name ||= "flexstub(#{obj.class.to_s})"
+ obj.instance_eval {
+ @flexmock_proxy ||= StubProxy.new(obj, FlexMock.new(name))
+ }
+ flexmock_remember(obj.instance_variable_get("@flexmock_proxy"))
+ end
+
+ # Intercept the named class in the target class for the duration
+ # of the test. Class interception is very simple-minded and has a
+ # number of restrictions. First, the intercepted class must be
+ # reference in the tested class via a simple constant name
+ # (e.g. no scoped names using "::") that is not directly defined
+ # in the class itself. After the test, a proxy class constant
+ # will be left behind that will forward all calls to the original
+ # class.
+ #
+ # Usage:
+ # intercept(SomeClass).in(ClassBeingTested).with(MockClass)
+ # intercept(SomeClass).with(MockClass).in(ClassBeingTested)
+ #
+ def intercept(intercepted_class)
+ result = Interception.new(intercepted_class)
+ @flexmock_interceptors ||= []
+ @flexmock_interceptors << result
+ result
+ end
+
+ private
+
+ def flexmock_remember(mocking_object)
+ @flexmock_created_mocks ||= []
+ @flexmock_created_mocks << mocking_object
+ mocking_object
+ end
+ end
+
+ ####################################################################
+ # A Class Interception defines a constant in the target class to be
+ # a proxy that points to a replacement class for the duration of a
+ # test. When an interception is restored, the proxy will point to
+ # the original intercepted class.
+ #
+ class Interception
+ # Create an interception object with the class to intercepted.
+ def initialize(intercepted_class)
+ @intercepted = nil
+ @target = nil
+ @replacement = nil
+ @proxy = nil
+ intercept(intercepted_class)
+ update
+ end
+
+ # Intercept this class in the class to be tested.
+ def intercept(intercepted_class)
+ @intercepted = intercepted_class
+ update
+ self
+ end
+
+ # Define the class number test that will receive the
+ # interceptioned definition.
+ def in(target_class)
+ @target = target_class
+ update
+ self
+ end
+
+ # Define the replacement class. This is normally a proxy or a
+ # stub.
+ def with(replacement_class)
+ @replacement = replacement_class
+ update
+ self
+ end
+
+ # Restore the original class. The proxy remains in place however.
+ def restore
+ @proxy.proxied_class = @restore_class if @proxy
+ end
+
+ private
+
+ # Update the interception if the definition is complete.
+ def update
+ if complete?
+ do_interception
+ end
+ end
+
+ # Is the interception definition complete. In other words, are
+ # all three actors defined?
+ def complete?
+ @intercepted && @target && @replacement
+ end
+
+ # Implement interception on the classes defined.
+ def do_interception
+ @target_class = coerce_class(@target, "target")
+ @replacement_class = coerce_class(@replacement, "replacement")
+ case @intercepted
+ when String, Symbol
+ @intercepted_name = @intercepted.to_s
+ when Class
+ @intercepted_name = @intercepted.name
+ end
+ @intercepted_class = coerce_class(@intercepted, "intercepted")
+ current_class = @target_class.const_get(@intercepted_name)
+ if ClassProxy === current_class
+ @proxy = current_class
+ @restore_class = @proxy.proxied_class
+ @proxy.proxied_class = @replacement_class
+ else
+ @proxy = ClassProxy.new(@replacement_class)
+ @restore_class = current_class
+ @target_class.const_set(@intercepted_name, @proxy)
+ end
+ end
+
+ # Coerce a class object, string to symbol to be the class object.
+ def coerce_class(klass, where)
+ case klass
+ when String, Symbol
+ lookup_const(klass.to_s, where)
+ else
+ klass
+ end
+ end
+
+ def lookup_const(name, where, target=Object)
+ begin
+ target.const_get(name)
+ rescue NameError
+ raise BadInterceptionError, "in #{where} class #{name}"
+ end
+ end
+ end
+
+ ####################################################################
+ # Class Proxy for class interception. Forward all method calls to
+ # whatever is the proxied_class.
+ #
+ class ClassProxy
+ attr_accessor :proxied_class
+ def initialize(default_class)
+ @proxied_class = default_class
+ end
+ def method_missing(sym, *args, &block)
+ @proxied_class.__send__(sym, *args, &block)
+ end
+ end
+
+ ####################################################################
+ # StubProxy is used to mate the mock framework to an existing
+ # object. The object is "enhanced" with a reference to a mock
+ # object (stored in <tt>@flexmock_mock</tt>). When the
+ # +should_receive+ method is sent to the proxy, it overrides the
+ # existing object's method by creating singleton method that
+ # forwards to the mock. When testing is complete, StubProxy
+ # will erase the mocking infrastructure from the object being
+ # stubbed (e.g. remove instance variables and mock singleton
+ # methods).
+ #
+ class StubProxy
+ attr_reader :mock
+
+ def initialize(obj, mock)
+ @obj = obj
+ @mock = mock
+ @method_definitions = {}
+ @methods_proxied = []
+ end
+
+ # Stub out the given method in the existing object and then let the
+ # mock object handle should_receive.
+ def should_receive(method_name)
+ method_name = method_name.to_sym
+ unless @methods_proxied.include?(method_name)
+ hide_existing_method(method_name)
+ @methods_proxied << method_name
+ end
+ @mock.should_receive(method_name)
+ end
+
+ # Verify that the mock has been properly called. After verification,
+ # detach the mocking infrastructure from the existing object.
+ def mock_verify
+ @mock.mock_verify
+ end
+
+ # Remove all traces of the mocking framework from the existing object.
+ def mock_teardown
+ if ! detached?
+ @methods_proxied.each do |method_name|
+ remove_current_method(method_name)
+ restore_original_definition(method_name)
+ end
+ @obj.instance_variable_set("@flexmock_proxy", nil)
+ @obj = nil
+ end
+ end
+
+ private
+
+ # The singleton class of the object.
+ def sclass
+ class << @obj; self; end
+ end
+
+ # Is the current method a singleton method in the object we are
+ # mocking?
+ def singleton?(method_name)
+ @obj.methods(false).include?(method_name.to_s)
+ end
+
+ # Hide the existing method definition with a singleton defintion
+ # that proxies to our mock object. If the current definition is a
+ # singleton, we need to record the definition and remove it before
+ # creating our own singleton method. If the current definition is
+ # not a singleton, all we need to do is override it with our own
+ # singleton.
+ def hide_existing_method(method_name)
+ if singleton?(method_name)
+ @method_definitions[method_name] = @obj.method(method_name)
+ remove_current_method(method_name)
+ end
+ define_proxy_method(method_name)
+ end
+
+ # Define a proxy method that forwards to our mock object. The
+ # proxy method is defined as a singleton method on the object
+ # being mocked.
+ def define_proxy_method(method_name)
+ sclass.class_eval %{
+ def #{method_name}(*args, &block)
+ @flexmock_proxy.mock.#{method_name}(*args, &block)
+ end
+ }
+ end
+
+ # Restore the original singleton defintion for method_name that
+ # was saved earlier.
+ def restore_original_definition(method_name)
+ method_def = @method_definitions[method_name]
+ if method_def
+ sclass.class_eval {
+ define_method(method_name, &method_def)
+ }
+ end
+ end
+
+ # Remove the current method if it is a singleton method of the
+ # object being mocked.
+ def remove_current_method(method_name)
+ sclass.class_eval { remove_method(method_name) }
+ end
+
+ # Have we been detached from the existing object?
+ def detached?
+ @obj.nil?
+ end
+
end
end