diff --git a/lib/rspec/mocks/method_double.rb b/lib/rspec/mocks/method_double.rb index 64b6a0336..f97941e79 100644 --- a/lib/rspec/mocks/method_double.rb +++ b/lib/rspec/mocks/method_double.rb @@ -3,7 +3,7 @@ module Mocks # @private class MethodDouble < Hash # @private - attr_reader :method_name, :object + attr_reader :method_name, :object, :original_method # @private def initialize(object, method_name, proxy) @@ -13,10 +13,25 @@ def initialize(object, method_name, proxy) @method_stasher = InstanceMethodStasher.new(object_singleton_class, @method_name) @method_is_proxied = false + @original_method = find_original_method store(:expectations, []) store(:stubs, []) end + def find_original_method + method_handle_name = if any_instance_class_recorder_observing_method?(@object.class) + ::RSpec::Mocks.any_instance_recorder_for(@object.class).build_alias_method_name(@method_name) + else + @method_name + end + + ::RSpec::Mocks.method_handle_for(@object, method_handle_name) + rescue NameError + Proc.new do |*args, &block| + @object.__send__(:method_missing, @method_name, *args, &block) + end + end + # @private def expectations self[:expectations] @@ -40,46 +55,6 @@ def visibility end end - class ProcWithBlock < Struct.new(:object, :method_name) - - def call(*args, &block) - self.object.__send__(:method_missing, self.method_name, *args, &block) - end - - end - - # @private - def original_method - if @method_stasher.method_is_stashed? - # Example: a singleton method defined on @object - ::RSpec::Mocks.method_handle_for(@object, @method_stasher.stashed_method_name) - elsif meth = original_unrecorded_any_instance_method - # Example: a method that has been mocked through - # klass.any_instance.should_receive(:msg).and_call_original - # any_instance.should_receive(:msg) causes the method to be - # replaced with a proxy method, and then `and_call_original` - # is recorded and played back on the object instance. We need - # special handling here to get a handle on the original method - # object rather than the proxy method. - meth - else - # Example: an instance method defined on one of @object's ancestors. - original_method_from_ancestry - 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. - ProcWithBlock.new(@object,@method_name) - end - - def original_unrecorded_any_instance_method - return nil unless any_instance_class_recorder_observing_method?(@object.class) - alias_name = ::RSpec::Mocks.any_instance_recorder_for(@object.class).build_alias_method_name(@method_name) - @object.method(alias_name) - end - def any_instance_class_recorder_observing_method?(klass) return true if ::RSpec::Mocks.any_instance_recorder_for(klass).already_observing?(@method_name) superklass = klass.superclass @@ -87,72 +62,6 @@ def any_instance_class_recorder_observing_method?(klass) any_instance_class_recorder_observing_method?(superklass) end - our_singleton_class = class << self; self; end - if our_singleton_class.ancestors.include? our_singleton_class - # In Ruby 2.1, ancestors include the correct ancestors, including the singleton classes - def original_method_from_ancestry - # Lookup in the ancestry, skipping over the singleton class itself - original_method_from_ancestor(object_singleton_class.ancestors.drop(1)) - end - else - # @private - def original_method_from_ancestry - original_method_from_ancestor(object_singleton_class.ancestors) - 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 - - def original_method_from_ancestor(ancestors) - klass, *rest = ancestors - klass.instance_method(@method_name).bind(@object) - rescue NameError - raise if rest.empty? - original_method_from_ancestor(rest) - 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 def object_singleton_class class << @object; self; end diff --git a/spec/rspec/mocks/and_call_original_spec.rb b/spec/rspec/mocks/and_call_original_spec.rb index 3cd07f096..82e986792 100644 --- a/spec/rspec/mocks/and_call_original_spec.rb +++ b/spec/rspec/mocks/and_call_original_spec.rb @@ -26,6 +26,12 @@ def self.new_instance expect(instance.meth_1).to eq(:original) end + it 'ignores prior declared stubs' do + instance.stub(:meth_1).and_return(:stubbed_value) + 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] } @@ -77,6 +83,15 @@ def instance.foo; :bar; end expect(sub_klass.foo).to eq(:sub_klass_bar) end + it "finds the method on the most direct singleton class ancestors even if the method " + + "is available on more distant ancestors" do + klass.extend Module.new { def foo; :klass_bar; end } + sub_klass = Class.new(klass) { def self.foo; :sub_klass_bar; end } + sub_sub_klass = Class.new(sub_klass) + sub_sub_klass.should_receive(:foo).and_call_original + expect(sub_sub_klass.foo).to eq(:sub_klass_bar) + end + context 'when using any_instance' do it 'works for instance methods defined on the class' do klass.any_instance.should_receive(:meth_1).and_call_original