Skip to content

Commit

Permalink
Distinguish between the original method and a method missing proc.
Browse files Browse the repository at this point in the history
Before, we would return a proc that invokes `method_missing`
from `original_method`, but it wasn’t a `Method` object
and couldn’t be bound, leading to `NoMethodError` in an
edge case.

Fixes #951.
  • Loading branch information
myronmarston committed May 21, 2015
1 parent 9aeba8b commit a4c4297
Show file tree
Hide file tree
Showing 5 changed files with 27 additions and 9 deletions.
2 changes: 2 additions & 0 deletions Changelog.md
Expand Up @@ -34,6 +34,8 @@ Bug Fixes:
* Fix `any_args`/`anything` support so that we avoid calling `obj == anything`
on user objects that may have improperly implemented `==` in a way that
raises errors. (Myron Marston, #924)
* Fix edge case involving stubbing the same method on a class and a subclass
which previously hit a `NoMethodError` internally in RSpec. (Myron Marston #954)

### 3.2.1 / 2015-02-23
[Full Changelog](http://github.com/rspec/rspec-mocks/compare/v3.2.0...v3.2.1)
Expand Down
2 changes: 1 addition & 1 deletion lib/rspec/mocks/message_expectation.rb
Expand Up @@ -118,7 +118,7 @@ def and_wrap_original(&block)
@error_generator.raise_only_valid_on_a_partial_double(:and_call_original)
else
warn_about_stub_override if implementation.inner_action
@implementation = AndWrapOriginalImplementation.new(@method_double.original_method, block)
@implementation = AndWrapOriginalImplementation.new(@method_double.original_implementation_callable, block)
@yield_receiver_to_implementation_block = false
end

Expand Down
16 changes: 10 additions & 6 deletions lib/rspec/mocks/method_double.rb
Expand Up @@ -18,20 +18,24 @@ def initialize(object, method_name, proxy)
@stubs = []
end

def original_method
def original_implementation_callable
# If original method is not present, uses the `method_missing`
# handler of the object. This accounts for cases where the user has not
# correctly defined `respond_to?`, and also 1.8 which does not provide
# method handles for missing methods even if `respond_to?` is correct.
@original_method ||=
@method_stasher.original_method ||
@proxy.original_method_handle_for(method_name) ||
@original_implementation_callable ||= original_method ||
Proc.new do |*args, &block|
@object.__send__(:method_missing, @method_name, *args, &block)
end
end

alias_method :save_original_method!, :original_method
alias_method :save_original_implementation_callable!, :original_implementation_callable

def original_method
@original_method ||=
@method_stasher.original_method ||
@proxy.original_method_handle_for(method_name)
end

# @private
def visibility
Expand All @@ -54,7 +58,7 @@ def configure_method
def define_proxy_method
return if @method_is_proxied

save_original_method!
save_original_implementation_callable!
definition_target.class_exec(self, method_name, visibility) do |method_double, method_name, visibility|
define_method(method_name) do |*args, &block|
method_double.proxy_method_invoked(self, *args, &block)
Expand Down
4 changes: 2 additions & 2 deletions lib/rspec/mocks/verifying_proxy.rb
Expand Up @@ -163,11 +163,11 @@ def initialize(object, method_name, proxy)

# Trigger an eager find of the original method since if we find it any
# later we end up getting a stubbed method with incorrect arity.
save_original_method!
save_original_implementation_callable!
end

def with_signature
yield Support::MethodSignature.new(original_method)
yield Support::MethodSignature.new(original_implementation_callable)
end

def unimplemented?
Expand Down
12 changes: 12 additions & 0 deletions spec/rspec/mocks/partial_double_spec.rb
Expand Up @@ -74,6 +74,18 @@ module Mocks
object.foobar
end

it 'allows a class and a subclass to both be stubbed' do
pending "Does not work on 1.8.7 due to singleton method restrictions" if RUBY_VERSION == "1.8.7"
the_klass = Class.new
the_subklass = Class.new(the_klass)

allow(the_klass).to receive(:foo).and_return(1)
allow(the_subklass).to receive(:foo).and_return(2)

expect(the_klass.foo).to eq(1)
expect(the_subklass.foo).to eq(2)
end

it "verifies the method was called when expecting a message" do
expect(object).to receive(:foobar).with(:test_param).and_return(1)
expect {
Expand Down

0 comments on commit a4c4297

Please sign in to comment.