Skip to content

Commit

Permalink
Get and_call_original to work properly on class hierarchies.
Browse files Browse the repository at this point in the history
For #613.
  • Loading branch information
myronmarston committed Apr 1, 2014
1 parent 1c5ec1a commit f0880f3
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 13 deletions.
2 changes: 1 addition & 1 deletion lib/rspec/mocks/method_double.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def original_method
# method handles for missing methods even if `respond_to?` is correct.
@original_method ||=
@method_stasher.original_method ||
@proxy.method_handle_for(method_name) ||
@proxy.original_method_handle_for(method_name) ||
Proc.new do |*args, &block|
@object.__send__(:method_missing, @method_name, *args, &block)
end
Expand Down
73 changes: 71 additions & 2 deletions lib/rspec/mocks/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def as_null_object
end

# @private
def method_handle_for(message)
def original_method_handle_for(message)
nil
end

Expand Down Expand Up @@ -255,7 +255,7 @@ def reset

# @private
class PartialDoubleProxy < Proxy
def method_handle_for(message)
def original_method_handle_for(message)
if any_instance_class_recorder_observing_method?(@object.class, message)
message = ::RSpec::Mocks.space.
any_instance_recorder_for(@object.class).
Expand Down Expand Up @@ -301,6 +301,75 @@ def any_instance_class_recorder_observing_method?(klass, method_name)
end
end

# @private
# When we mock or stub a method on a class, we have to treat it a bit different,
# because normally singleton method definitions only affect the object on which
# they are defined, but on classes they affect subclasses, too. As a result,
# we need some special handling to get the original method.
module PartialClassDoubleProxyMethods
def initialize(source_space, *args)
@source_space = source_space
super(*args)
end

# Consider this situation:
#
# class A; end
# class B < A; end
#
# allow(A).to receive(:new)
# expect(B).to receive(:new).and_call_original
#
# When getting the original definition for `B.new`, we cannot rely purely on
# using `B.method(:new)` before our redefinition is defined on `B`, because
# `B.method(:new)` will return a method that will execute the stubbed version
# of the method on `A` since singleton methods on classes are in the lookup
# hierarchy.
#
# To do it properly, we need to find the original definition of `new` from `A`
# from _before_ `A` was stubbed, and we need to rebind it to `B` so that it will
# run with the proper `self`.
#
# That's what this method (together with `original_unbound_method_handle_from_ancestor_for`)
# does.
def original_method_handle_for(message)
unbound_method = superclass_proxy &&
superclass_proxy.original_unbound_method_handle_from_ancestor_for(message.to_sym)

return super unless unbound_method
unbound_method.bind(object)
end

protected

def original_unbound_method_handle_from_ancestor_for(message)
method_double = @method_doubles.fetch(message) do
# The fact that there is no method double for this message indicates
# that it has not been redefined by rspec-mocks. We need to continue
# looking up the ancestor chain.
return superclass_proxy &&
superclass_proxy.original_unbound_method_handle_from_ancestor_for(message)
end

method_double.original_method.unbind
end

def superclass_proxy
return @superclass_proxy if defined?(@superclass_proxy)

if (superclass = object.superclass)
@superclass_proxy = @source_space.proxy_for(superclass)
else
@superclass_proxy = nil
end
end
end

# @private
class PartialClassDoubleProxy < PartialDoubleProxy
include PartialClassDoubleProxyMethods
end

# @private
class ProxyForNil < PartialDoubleProxy
def initialize(order_group)
Expand Down
6 changes: 6 additions & 0 deletions lib/rspec/mocks/space.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ def proxy_not_found_for(id, object)
proxies[id] = case object
when NilClass then ProxyForNil.new(@expectation_ordering)
when TestDouble then object.__build_mock_proxy(@expectation_ordering)
when Class
if RSpec::Mocks.configuration.verify_partial_doubles?
VerifyingPartialClassDoubleProxy.new(self, object, @expectation_ordering)
else
PartialClassDoubleProxy.new(self, object, @expectation_ordering)
end
else
if RSpec::Mocks.configuration.verify_partial_doubles?
VerifyingPartialDoubleProxy.new(object, @expectation_ordering)
Expand Down
5 changes: 5 additions & 0 deletions lib/rspec/mocks/verifying_proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ def method_reference
end
end

# @private
class VerifyingPartialClassDoubleProxy < VerifyingPartialDoubleProxy
include PartialClassDoubleProxyMethods
end

# @private
class VerifyingMethodDouble < MethodDouble
def initialize(object, method_name, proxy, method_reference)
Expand Down
46 changes: 36 additions & 10 deletions spec/rspec/mocks/stub_implementation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,44 @@ def obj.foo; :original; end
expect(obj.foo(3)).to eq :three
end

it "restores the correct implementations when stubbed and unstubbed on a parent and child class" do
parent = Class.new
child = Class.new(parent)
shared_examples_for "stubbing `new` on class objects" do
it "restores the correct implementations when stubbed and unstubbed on a parent and child class" do
parent = stub_const("Parent", Class.new)
child = stub_const("Child", Class.new(parent))

allow(parent).to receive(:new)
allow(child).to receive(:new)
allow(parent).to receive(:new).and_call_original
allow(child).to receive(:new).and_call_original
allow(parent).to receive(:new)
allow(child).to receive(:new)
allow(parent).to receive(:new).and_call_original
allow(child).to receive(:new).and_call_original

expect(parent.new).to be_an_instance_of parent
pending "not working for `and_call_original` yet, but works with `unstub`"
expect(child.new).to be_an_instance_of child
expect(parent.new).to be_an_instance_of parent
expect(child.new).to be_an_instance_of child
end

it "restores the correct implementations when stubbed and unstubbed on a grandparent and grandchild class" do
grandparent = stub_const("GrandParent", Class.new)
parent = stub_const("Parent", Class.new(grandparent))
child = stub_const("Child", Class.new(parent))

allow(grandparent).to receive(:new)
allow(child).to receive(:new)
allow(grandparent).to receive(:new).and_call_original
allow(child).to receive(:new).and_call_original

expect(grandparent.new).to be_an_instance_of grandparent
expect(child.new).to be_an_instance_of child
end
end

context "when partial doubles are not verified" do
before { expect(RSpec::Mocks.configuration.verify_partial_doubles?).to be false }
include_examples "stubbing `new` on class objects"
end

context "when partial doubles are verified" do
include_context "with isolated configuration"
before { RSpec::Mocks.configuration.verify_partial_doubles = true }
include_examples "stubbing `new` on class objects"
end
end
end
Expand Down

0 comments on commit f0880f3

Please sign in to comment.