Skip to content

Commit

Permalink
Fix stubbing prepended only methods
Browse files Browse the repository at this point in the history
Previously, we're assuming the method must be defined in the
singleton class. However this is not always true. Whenever the
method was only defined in the prepended module, then it's not
defined in the singleton class. We need to find the owner of
the method instead, which is the prepended module.

Closes rspec#1213
  • Loading branch information
godfat authored and pirj committed Mar 16, 2021
1 parent a692dd4 commit ef1197f
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 7 deletions.
4 changes: 3 additions & 1 deletion lib/rspec/mocks/instance_method_stasher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ def method_owned_by_klass?
# `#<MyClass:0x007fbb94e3cd10>`, rather than the expected `MyClass`.
owner = owner.class unless Module === owner

owner == @klass || !(method_defined_on_klass?(owner))
owner == @klass ||
owner.singleton_class == @klass || # When `extend self` is used
!(method_defined_on_klass?(owner))
end
end
end
Expand Down
25 changes: 19 additions & 6 deletions lib/rspec/mocks/method_double.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,11 @@ def restore_original_method
return unless @method_is_proxied

remove_method_from_definition_target
@method_stasher.restore if @method_stasher.method_is_stashed?
restore_original_visibility

if @method_stasher.method_is_stashed?
@method_stasher.restore
restore_original_visibility
end

@method_is_proxied = false
end
Expand All @@ -104,10 +107,7 @@ def show_frozen_warning

# @private
def restore_original_visibility
return unless @original_visibility &&
MethodReference.method_defined_at_any_visibility?(object_singleton_class, @method_name)

object_singleton_class.__send__(@original_visibility, method_name)
method_owner.__send__(@original_visibility, @method_name)
end

# @private
Expand Down Expand Up @@ -249,6 +249,19 @@ def new_rspec_prepended_module
end
end

def method_owner
@method_owner ||=
if Object === object
# This works in Ruby 2.3+, so we can just use this after we
# drop Ruby 2.2 and earlier.
Object.instance_method(:method).bind(object).call(@method_name).owner
else
# BasicObject or SimpleDelegator might not be able to bind to
# Object in Ruby 2.2 and earlier.
object.method(@method_name).owner
end
end

def remove_method_from_definition_target
# In Ruby 2.4 and earlier, `remove_method` is private
definition_target.__send__(:remove_method, @method_name)
Expand Down
13 changes: 13 additions & 0 deletions spec/rspec/mocks/stub_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ module ToBePrepended
def value
"#{super}_prepended".to_sym
end

def value_without_super
:prepended
end
end

it "handles stubbing prepended methods" do
Expand Down Expand Up @@ -165,6 +169,15 @@ def object.value; :original; end
expect(object.value).to eq :stubbed
end

it "handles stubbing prepending methods that were only defined on the prepended module" do
object = Object.new
object.singleton_class.send(:prepend, ToBePrepended)

expect(object.value_without_super).to eq :prepended
allow(object).to receive(:value_without_super) { :stubbed }
expect(object.value_without_super).to eq :stubbed
end

it 'does not unnecessarily prepend a module when the prepended module does not override the stubbed method' do
object = Object.new
def object.value; :original; end
Expand Down

0 comments on commit ef1197f

Please sign in to comment.