Skip to content
Browse files

Merge pull request #23 from myronmarston/can_call_original

Add method to call_original method from a stub
  • Loading branch information...
2 parents 22d18ce + fafb085 commit 3d278bc22b380c0f5f733b76889ef0112be1572a @myronmarston myronmarston committed
View
2 Changelog.md
@@ -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
12 README.md
@@ -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
View
4 features/message_expectations/README.md
@@ -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")
View
24 features/message_expectations/call_original.feature
@@ -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
+
View
6 lib/rspec/mocks/error_generator.rb
@@ -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
View
2 lib/rspec/mocks/framework.rb
@@ -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'
View
11 lib/rspec/mocks/stashed_instance_method.rb → lib/rspec/mocks/instance_method_stasher.rb
@@ -1,7 +1,7 @@
module RSpec
module Mocks
# @private
- class StashedInstanceMethod
+ class InstanceMethodStasher
def initialize(klass, method)
@klass = klass
@method = method
@@ -10,6 +10,11 @@ def initialize(klass, method)
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
View
29 lib/rspec/mocks/message_expectation.rb
@@ -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(*)
View
91 lib/rspec/mocks/method_double.rb
@@ -3,7 +3,7 @@ module Mocks
# @private
class MethodDouble < Hash
# @private
- attr_reader :method_name
+ attr_reader :method_name, :object
# @private
def initialize(object, method_name, proxy)
@@ -11,7 +11,7 @@ def initialize(object, method_name, proxy)
@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, [])
@@ -41,6 +41,80 @@ def visibility
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
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,7 +188,8 @@ 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
@@ -121,7 +197,8 @@ def add_negative_expectation(error_generator, expectation_ordering, expected_fro
# @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
View
162 spec/rspec/mocks/and_call_original_spec.rb
@@ -0,0 +1,162 @@
+require 'spec_helper'
+
+describe "and_call_original" do
+ context "on a partial mock object" do
+ let(:klass) do
+ Class.new do
+ def meth_1
+ :original
+ end
+
+ def meth_2(x)
+ yield x, :additional_yielded_arg
+ end
+
+ def self.new_instance
+ new
+ end
+ end
+ end
+
+ let(:instance) { klass.new }
+
+ it 'passes the received message through to the original method' do
+ instance.should_receive(:meth_1).and_call_original
+ expect(instance.meth_1).to eq(:original)
+ end
+
+ it 'passes args and blocks through to the original method' do
+ instance.should_receive(:meth_2).and_call_original
+ value = instance.meth_2(:submitted_arg) { |a, b| [a, b] }
+ expect(value).to eq([:submitted_arg, :additional_yielded_arg])
+ end
+
+ it 'works for singleton methods' do
+ def instance.foo; :bar; end
+ instance.should_receive(:foo).and_call_original
+ expect(instance.foo).to eq(:bar)
+ end
+
+ if RUBY_VERSION.to_f > 1.8
+ it 'works for class methods defined on a superclass' do
+ subclass = Class.new(klass)
+ subclass.should_receive(:new_instance).and_call_original
+ expect(subclass.new_instance).to be_a(subclass)
+ end
+
+ it 'works for class methods defined on a grandparent class' do
+ sub_subclass = Class.new(Class.new(klass))
+ sub_subclass.should_receive(:new_instance).and_call_original
+ expect(sub_subclass.new_instance).to be_a(sub_subclass)
+ end
+ else
+ it 'attempts to work for class methods defined on a superclass but ' +
+ 'executes the method with `self` as the superclass' do
+ ::Kernel.stub(:warn)
+ subclass = Class.new(klass)
+ subclass.should_receive(:new_instance).and_call_original
+ expect(subclass.new_instance).to be_an_instance_of(klass)
+ end
+
+ it 'prints a warning to notify users that `self` will not be correct' do
+ subclass = Class.new(klass)
+ ::Kernel.should_receive(:warn).with(/may not work correctly/)
+ subclass.should_receive(:new_instance).and_call_original
+ subclass.new_instance
+ end
+ end
+
+ it 'works for class methods defined on the Class class' do
+ klass.should_receive(:new).and_call_original
+ expect(klass.new).to be_an_instance_of(klass)
+ end
+
+ it "works for instance methods defined on the object's class's superclass" do
+ subclass = Class.new(klass)
+ inst = subclass.new
+ inst.should_receive(:meth_1).and_call_original
+ expect(inst.meth_1).to eq(:original)
+ end
+
+ context 'on an object that defines method_missing' do
+ before do
+ klass.class_eval do
+ private
+
+ def method_missing(name, *args)
+ if name.to_s == "greet_jack"
+ "Hello, jack"
+ else
+ super
+ end
+ end
+ end
+ end
+
+ it 'works when the method_missing definition handles the message' do
+ instance.should_receive(:greet_jack).and_call_original
+ expect(instance.greet_jack).to eq("Hello, jack")
+ end
+
+ it 'raises an error on invocation if method_missing does not handle the message' do
+ instance.should_receive(:not_a_handled_message).and_call_original
+
+ # Note: it should raise a NoMethodError (and usually does), but
+ # due to a weird rspec-expectations issue (see #183) it sometimes
+ # raises a `NameError` when a `be_xxx` predicate matcher has been
+ # recently used. `NameError` is the superclass of `NoMethodError`
+ # so this example will pass regardless.
+ # If/when we solve the rspec-expectations issue, this can (and should)
+ # be changed to `NoMethodError`.
+ expect {
+ instance.not_a_handled_message
+ }.to raise_error(NameError, /not_a_handled_message/)
+ end
+ end
+ end
+
+ context "on a partial mock object that overrides #method" do
+ let(:request_klass) do
+ Struct.new(:method, :url) do
+ def perform
+ :the_response
+ end
+
+ def self.method
+ :some_method
+ end
+ end
+ end
+
+ let(:request) { request_klass.new(:get, "http://foo.com/bar") }
+
+ it 'still works even though #method has been overriden' do
+ request.should_receive(:perform).and_call_original
+ expect(request.perform).to eq(:the_response)
+ end
+
+ it 'works for a singleton method' do
+ def request.perform
+ :a_response
+ end
+
+ request.should_receive(:perform).and_call_original
+ expect(request.perform).to eq(:a_response)
+ end
+ end
+
+ context "on a pure mock object" do
+ let(:instance) { double }
+
+ it 'raises an error even if the mock object responds to the message' do
+ expect(instance.to_s).to be_a(String)
+ mock_expectation = instance.should_receive(:to_s)
+ instance.to_s # to satisfy the expectation
+
+ expect {
+ mock_expectation.and_call_original
+ }.to raise_error(/and_call_original.*partial mock/i)
+ end
+ end
+end
+
View
8 ...pec/mocks/stashed_instance_method_spec.rb → ...pec/mocks/instance_method_stasher_spec.rb
@@ -2,7 +2,7 @@
module RSpec
module Mocks
- describe StashedInstanceMethod do
+ describe InstanceMethodStasher do
class ExampleClass
def hello
:hello_defined_on_class
@@ -17,7 +17,7 @@ class << obj; self; end
obj = Object.new
def obj.hello; :hello_defined_on_singleton_class; end;
- stashed_method = StashedInstanceMethod.new(singleton_class_for(obj), :hello)
+ stashed_method = InstanceMethodStasher.new(singleton_class_for(obj), :hello)
stashed_method.stash
def obj.hello; :overridden_hello; end
@@ -32,7 +32,7 @@ def obj.hello; :overridden_hello; end
def obj.hello; :hello_defined_on_singleton_class; end;
singleton_class_for(obj).__send__(:private, :hello)
- stashed_method = StashedInstanceMethod.new(singleton_class_for(obj), :hello)
+ stashed_method = InstanceMethodStasher.new(singleton_class_for(obj), :hello)
stashed_method.stash
def obj.hello; :overridden_hello; end
@@ -43,7 +43,7 @@ def obj.hello; :overridden_hello; end
it "only stashes methods directly defined on the given class, not its ancestors" do
obj = ExampleClass.new
- stashed_method = StashedInstanceMethod.new(singleton_class_for(obj), :hello)
+ stashed_method = InstanceMethodStasher.new(singleton_class_for(obj), :hello)
stashed_method.stash
def obj.hello; :overridden_hello; end;

0 comments on commit 3d278bc

Please sign in to comment.
Something went wrong with that request. Please try again.