Permalink
Browse files

Merge pull request #23 from myronmarston/can_call_original

Add method to call_original method from a stub
  • Loading branch information...
myronmarston committed Oct 26, 2012
2 parents 22d18ce + fafb085 commit 3d278bc22b380c0f5f733b76889ef0112be1572a
View
@@ -6,6 +6,8 @@ Enhancements
* and_raise can accept an exception class and message, more closely
matching Kernel#raise (e.g., foo.stub(:bar).and_raise(RuntimeError, "message"))
(Bas Vodde)
+* Add `and_call_original`, which will delegate the message to the
+ original method (Myron Marston).
Deprecations:
View
@@ -246,6 +246,18 @@ double.should_receive(:msg) do |&arg|
end
```
+## Delegating to the Original Implementation
+
+When working with a partial mock object, you may occasionally
+want to set a message expecation without interfering with how
+the object responds to the message. You can use `and_call_original`
+to achieve this:
+
+```ruby
+Person.should_receive(:find).and_call_original
+Person.find # => executes the original find method and returns the result
+```
+
## Combining Expectation Details
Combining the message name with specific arguments, receive counts and responses
@@ -22,6 +22,10 @@ block contents are evaluated lazily when the `obj` receives the
# and set a return value
end
+### Using the original implementation
+
+ obj.should_receive(:message).and_call_original
+
### Raising/Throwing
obj.should_receive(:message).and_raise("this error")
@@ -0,0 +1,24 @@
+Feature: Calling the original method
+
+ You can use `and_call_original` on the fluent interface
+ to "pass through" the received message to the original method.
+
+ Scenario: expect a message
+ Given a file named "call_original_spec.rb" with:
+ """ruby
+ class Addition
+ def self.two_plus_two
+ 4
+ end
+ end
+
+ describe "and_call_original" do
+ it "delegates the message to the original implementation" do
+ Addition.should_receive(:two_plus_two).and_call_original
+ Addition.two_plus_two.should eq(4)
+ end
+ end
+ """
+ When I run `rspec call_original_spec.rb`
+ Then the examples should all pass
+
@@ -66,6 +66,12 @@ def raise_wrong_arity_error(args_to_yield, arity)
__raise "#{intro} yielded |#{arg_list(*args_to_yield)}| to block with arity of #{arity}"
end
+ # @private
+ def raise_only_valid_on_a_partial_mock(method)
+ __raise "#{intro} is a pure mock object. `#{method}` is only " +
+ "available on a partial mock object."
+ end
+
private
def intro
@@ -4,7 +4,7 @@
require 'rspec/mocks/configuration'
require 'rspec/mocks/extensions/instance_exec'
-require 'rspec/mocks/stashed_instance_method'
+require 'rspec/mocks/instance_method_stasher'
require 'rspec/mocks/method_double'
require 'rspec/mocks/methods'
require 'rspec/mocks/argument_matchers'
@@ -1,14 +1,19 @@
module RSpec
module Mocks
# @private
- class StashedInstanceMethod
+ class InstanceMethodStasher
def initialize(klass, method)
@klass = klass
@method = method
@method_is_stashed = false
end
+ # @private
+ def method_is_stashed?
+ @method_is_stashed
+ end
+
# @private
def stash
return if !method_defined_directly_on_klass? || @method_is_stashed
@@ -44,13 +49,13 @@ def method_owned_by_klass?
end
end
+ public
+
# @private
def stashed_method_name
"obfuscated_by_rspec_mocks__#{@method}"
end
- public
-
# @private
def restore
return unless @method_is_stashed
@@ -9,11 +9,13 @@ class MessageExpectation
protected :expected_received_count=, :expected_from=, :error_generator, :error_generator=
# @private
- def initialize(error_generator, expectation_ordering, expected_from, message, expected_received_count=1, opts={}, &implementation)
+ def initialize(error_generator, expectation_ordering, expected_from, method_double,
+ expected_received_count=1, opts={}, &implementation)
@error_generator = error_generator
@error_generator.opts = opts
@expected_from = expected_from
- @message = message
+ @method_double = method_double
+ @message = @method_double.method_name
@actual_received_count = 0
@expected_received_count = expected_received_count
@argument_list_matcher = ArgumentListMatcher.new(ArgumentMatchers::AnyArgsMatcher.new)
@@ -97,6 +99,25 @@ def and_return(*values, &implementation)
@implementation = implementation || build_implementation(values)
end
+ # Tells the object to delegate to the original unmodified method
+ # when it receives the message.
+ #
+ # @note This is only available on partial mock objects.
+ #
+ # @example
+ #
+ # counter.should_receive(:increment).and_call_original
+ # original_count = counter.count
+ # counter.increment
+ # expect(counter.count).to eq(original_count + 1)
+ def and_call_original
+ if @method_double.object.is_a?(RSpec::Mocks::TestDouble)
+ @error_generator.raise_only_valid_on_a_partial_mock(:and_call_original)
+ else
+ self.implementation = @method_double.original_method
+ end
+ end
+
# @overload and_raise
# @overload and_raise(ExceptionClass)
# @overload and_raise(ExceptionClass, message)
@@ -465,8 +486,8 @@ def build_implementation(values)
# @private
class NegativeMessageExpectation < MessageExpectation
# @private
- def initialize(error_generator, expectation_ordering, expected_from, message, &implementation)
- super(error_generator, expectation_ordering, expected_from, message, 0, {}, &implementation)
+ def initialize(error_generator, expectation_ordering, expected_from, method_double, &implementation)
+ super(error_generator, expectation_ordering, expected_from, method_double, 0, {}, &implementation)
end
def and_return(*)
@@ -3,15 +3,15 @@ module Mocks
# @private
class MethodDouble < Hash
# @private
- attr_reader :method_name
+ attr_reader :method_name, :object
# @private
def initialize(object, method_name, proxy)
@method_name = method_name
@object = object
@proxy = proxy
- @stashed_method = StashedInstanceMethod.new(object_singleton_class, @method_name)
+ @method_stasher = InstanceMethodStasher.new(object_singleton_class, @method_name)
@method_is_proxied = false
store(:expectations, [])
store(:stubs, [])
@@ -40,6 +40,80 @@ def visibility
end
end
+ # @private
+ def original_method
+ if @method_stasher.method_is_stashed?
+ # Example: a singleton method defined on @object
+ method_handle_for(@object, @method_stasher.stashed_method_name)
+ else
+ begin
+ # Example: an instance method defined on @object's class.
+ @object.class.instance_method(@method_name).bind(@object)
+ rescue NameError
+ raise unless @object.respond_to?(:superclass)
+
+ # Example: a singleton method defined on @object's superclass.
+ #
+ # Note: we have to give precedence to instance methods
+ # defined on @object's class, because in a case like:
+ #
+ # `klass.should_receive(:new).and_call_original`
+ #
+ # ...we want `Class#new` bound to `klass` (which will return
+ # an instance of `klass`), not `klass.superclass.new` (which
+ # would return an instance of `klass.superclass`).
+ original_method_from_superclass
+ end
+ end
+ rescue NameError
+ # We have no way of knowing if the object's method_missing
+ # will handle this message or not...but we can at least try.
+ # If it's not handled, a `NoMethodError` will be raised, just
+ # like normally.
+ Proc.new do |*args, &block|
+ @object.__send__(:method_missing, @method_name, *args, &block)
+ end
+ end
+
+ if RUBY_VERSION.to_f > 1.8
+ # @private
+ def original_method_from_superclass
+ @object.superclass.
+ singleton_class.
+ instance_method(@method_name).
+ bind(@object)
+ end
+ else
+ # Our implementation for 1.9 (above) causes an error on 1.8:
+ # TypeError: singleton method bound for a different object
+ #
+ # This doesn't work quite right in all circumstances but it's the
+ # best we can do.
+ # @private
+ def original_method_from_superclass
+ ::Kernel.warn <<-WARNING.gsub(/^ +\|/, '')
+ |
+ |WARNING: On ruby 1.8, rspec-mocks is unable to bind the original
+ |`#{@method_name}` method to your partial mock object (#{@object})
+ |for `and_call_original`. The superclass's `#{@method_name}` is being
+ |used instead; however, it may not work correctly when executed due
+ |to the fact that `self` will be #{@object.superclass}, not #{@object}.
+ |
+ |Called from: #{caller[2]}
+ WARNING
+
+ @object.superclass.method(@method_name)
+ end
+ end
+
+ # @private
+ OBJECT_METHOD_METHOD = ::Object.instance_method(:method)
+
+ # @private
+ def method_handle_for(object, method_name)
+ OBJECT_METHOD_METHOD.bind(object).call(method_name)
+ end
+
# @private
def object_singleton_class
class << @object; self; end
@@ -49,7 +123,7 @@ class << @object; self; end
def configure_method
RSpec::Mocks::space.add(@object) if RSpec::Mocks::space
warn_if_nil_class
- @stashed_method.stash unless @method_is_proxied
+ @method_stasher.stash unless @method_is_proxied
define_proxy_method
end
@@ -76,7 +150,7 @@ def restore_original_method
return unless @method_is_proxied
object_singleton_class.__send__(:remove_method, @method_name)
- @stashed_method.restore
+ @method_stasher.restore
@method_is_proxied = false
end
@@ -104,7 +178,8 @@ def add_expectation(error_generator, expectation_ordering, expected_from, opts,
expectation = if existing_stub = stubs.first
existing_stub.build_child(expected_from, 1, opts, &implementation)
else
- MessageExpectation.new(error_generator, expectation_ordering, expected_from, @method_name, 1, opts, &implementation)
+ MessageExpectation.new(error_generator, expectation_ordering,
+ expected_from, self, 1, opts, &implementation)
end
expectations << expectation
expectation
@@ -113,15 +188,17 @@ def add_expectation(error_generator, expectation_ordering, expected_from, opts,
# @private
def add_negative_expectation(error_generator, expectation_ordering, expected_from, &implementation)
configure_method
- expectation = NegativeMessageExpectation.new(error_generator, expectation_ordering, expected_from, @method_name, &implementation)
+ expectation = NegativeMessageExpectation.new(error_generator, expectation_ordering,
+ expected_from, self, &implementation)
expectations.unshift expectation
expectation
end
# @private
def add_stub(error_generator, expectation_ordering, expected_from, opts={}, &implementation)
configure_method
- stub = MessageExpectation.new(error_generator, expectation_ordering, expected_from, @method_name, :any, opts, &implementation)
+ stub = MessageExpectation.new(error_generator, expectation_ordering, expected_from,
+ self, :any, opts, &implementation)
stubs.unshift stub
stub
end
Oops, something went wrong.

0 comments on commit 3d278bc

Please sign in to comment.