Skip to content

Commit

Permalink
Consider stubs on superclasses if none exist on receiver. Fixes #145.
Browse files Browse the repository at this point in the history
Note: This may break existing tests which rely on the old behaviour!

Stubbing a superclass method and then invoking that method on a child
class would previously cause an unexpected invocation error. This was
because the superclass and child class delegated to different mock
objects i.e. the superclass has its own `@mocha` class instance variable
which is different from the child class' `@mocha` class instance
variable.

By searching up through the inheritance hierarchy for each `@mocha`
instance variable, we can provide a more intuitive behaviour. Instead of
an unexpected invocation error (see above), invoking the method on the
child class will cause the stubbed method on the superclass to be used.

    class Parent
      def self.my_class_method
        :original_value
      end
    end

    class Child < Parent
    end

    Parent.stubs(:my_class_method).returns(:stubbed_value)

    # old behaviour
    Child.my_class_method # => unexpected invocation error

    # new behaviour
    Child.my_class_method # => :stubbed_value

For consistency, I have also implemented a similar change for the
corresponding `any_instance` scenario:

    class Parent
      def my_instance_method
        :original_value
      end
    end

    class Child < Parent
    end

    Parent.any_instance.stubs(:my_instance_method).returns(:stubbed_value)

    # old behaviour
    Child.new.my_instance_method # => unexpected invocation error

    # new behaviour
    Child.new.my_instance_method # => :stubbed_value

These changes were based in part on a suggestion by @ccutrer.
  • Loading branch information
floehopper committed Mar 19, 2013
1 parent b500ce8 commit b41254a
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 4 deletions.
17 changes: 16 additions & 1 deletion lib/mocha/class_methods.rb
Expand Up @@ -7,6 +7,16 @@ module Mocha
# Methods added to all classes to allow mocking and stubbing on real (i.e. non-mock) objects.
module ClassMethods

# @private
def mocha
mocha, klass = nil, self
while mocha.nil? && klass.respond_to?(:superclass)
mocha = klass.instance_variable_defined?(:@mocha) ? klass.instance_variable_get(:@mocha) : nil
klass = klass.superclass
end
mocha || @mocha = Mocha::Mockery.instance.mock_impersonating(self)
end

# @private
def stubba_method
Mocha::ClassMethod
Expand All @@ -20,7 +30,12 @@ def initialize(klass)
end

def mocha
@mocha ||= Mocha::Mockery.instance.mock_impersonating_any_instance_of(@stubba_object)
mocha, klass = nil, stubba_object
while mocha.nil? && klass.respond_to?(:superclass)
mocha = klass.any_instance.instance_variable_defined?(:@mocha) ? klass.any_instance.instance_variable_get(:@mocha) : nil
klass = klass.superclass
end
mocha || @mocha = Mocha::Mockery.instance.mock_impersonating_any_instance_of(@stubba_object)
end

def stubba_method
Expand Down
@@ -0,0 +1,34 @@
require File.expand_path('../acceptance_test_helper', __FILE__)
require 'mocha/setup'

class StubAnyInstanceMethodDefinedOnSuperclassTest < Test::Unit::TestCase

include AcceptanceTest

def setup
setup_acceptance_test
end

def teardown
teardown_acceptance_test
end

def test_should_stub_method_and_leave_it_unchanged_after_test
superklass = Class.new do
def my_superclass_method
:original_return_value
end
public :my_superclass_method
end
klass = Class.new(superklass)
instance = klass.new
assert_snapshot_unchanged(instance) do
test_result = run_as_test do
superklass.any_instance.stubs(:my_superclass_method).returns(:new_return_value)
assert_equal :new_return_value, instance.my_superclass_method
end
assert_passed(test_result)
end
assert_equal :original_return_value, instance.my_superclass_method
end
end
26 changes: 23 additions & 3 deletions test/acceptance/stub_class_method_defined_on_superclass_test.rb
Expand Up @@ -13,7 +13,7 @@ def teardown
teardown_acceptance_test
end

def test_should_stub_public_method_and_leave_it_unchanged_after_test
def test_should_stub_public_method_on_child_class_and_leave_it_unchanged_after_test
superklass = Class.new do
class << self
def my_class_method
Expand All @@ -33,7 +33,7 @@ def my_class_method
assert_equal :original_return_value, klass.my_class_method
end

def test_should_stub_protected_method_and_leave_it_unchanged_after_test
def test_should_stub_protected_method_on_child_class_and_leave_it_unchanged_after_test
superklass = Class.new do
class << self
def my_class_method
Expand All @@ -53,7 +53,7 @@ def my_class_method
assert_equal :original_return_value, klass.send(:my_class_method)
end

def test_should_stub_private_method_and_leave_it_unchanged_after_test
def test_should_stub_private_method_on_child_class_and_leave_it_unchanged_after_test
superklass = Class.new do
class << self
def my_class_method
Expand All @@ -72,4 +72,24 @@ def my_class_method
end
assert_equal :original_return_value, klass.send(:my_class_method)
end

def test_should_stub_method_on_superclass_and_leave_it_unchanged_after_test
superklass = Class.new do
class << self
def my_class_method
:original_return_value
end
public :my_class_method
end
end
klass = Class.new(superklass)
assert_snapshot_unchanged(klass) do
test_result = run_as_test do
superklass.stubs(:my_class_method).returns(:new_return_value)
assert_equal :new_return_value, klass.my_class_method
end
assert_passed(test_result)
end
assert_equal :original_return_value, klass.my_class_method
end
end

0 comments on commit b41254a

Please sign in to comment.