Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Consider stubs on superclasses if none exist on primary receiver.

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 of the delegate
mock objects, we can provide 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 largely based on changes suggested by @ccutrer in #145.
  • Loading branch information...
commit f33f506492f0536b232148fa059a41c95349d96a 1 parent 208ba82
James Mead floehopper authored
8 lib/mocha/expectation_list.rb
View
@@ -2,8 +2,8 @@ module Mocha
class ExpectationList
- def initialize
- @expectations = []
+ def initialize(expectations = [])
+ @expectations = expectations
end
def add(expectation)
@@ -47,6 +47,10 @@ def any?
@expectations.any?
end
+ def +(other)
+ self.class.new(self.to_a + other.to_a)
+ end
+
private
def matching_expectations(method_name, *arguments)
15 lib/mocha/mock.rb
View
@@ -2,6 +2,7 @@
require 'mocha/expectation'
require 'mocha/expectation_list'
require 'mocha/names'
+require 'mocha/receivers'
require 'mocha/method_matcher'
require 'mocha/parameters_matcher'
require 'mocha/unexpected_invocation'
@@ -246,9 +247,10 @@ def responds_like_instance_of(responder_class)
end
# @private
- def initialize(mockery, name = nil, &block)
+ def initialize(mockery, name = nil, receiver = nil, &block)
@mockery = mockery
@name = name || DefaultName.new(self)
+ @receiver = receiver || DefaultReceiver.new(self)
@expectations = ExpectationList.new
@everything_stubbed = false
@responder = nil
@@ -277,14 +279,19 @@ def stub_everything
end
# @private
+ def all_expectations
+ @receiver.mocks.inject(ExpectationList.new) { |e, m| e + m.__expectations__ }
+ end
+
+ # @private
def method_missing(symbol, *arguments, &block)
if @responder and not @responder.respond_to?(symbol)
raise NoMethodError, "undefined method `#{symbol}' for #{self.mocha_inspect} which responds like #{@responder.mocha_inspect}"
end
- if matching_expectation_allowing_invocation = @expectations.match_allowing_invocation(symbol, *arguments)
+ if matching_expectation_allowing_invocation = all_expectations.match_allowing_invocation(symbol, *arguments)
matching_expectation_allowing_invocation.invoke(&block)
else
- if (matching_expectation = @expectations.match(symbol, *arguments)) || (!matching_expectation && !@everything_stubbed)
+ if (matching_expectation = all_expectations.match(symbol, *arguments)) || (!matching_expectation && !@everything_stubbed)
if @unexpected_invocation.nil?
@unexpected_invocation = UnexpectedInvocation.new(self, symbol, *arguments)
matching_expectation.invoke(&block) if matching_expectation
@@ -307,7 +314,7 @@ def respond_to?(symbol, include_private = false)
@responder.respond_to?(symbol)
end
else
- @everything_stubbed || @expectations.matches_method?(symbol)
+ @everything_stubbed || all_expectations.matches_method?(symbol)
end
end
5 lib/mocha/mockery.rb
View
@@ -1,6 +1,7 @@
require 'mocha/central'
require 'mocha/mock'
require 'mocha/names'
+require 'mocha/receivers'
require 'mocha/state_machine'
require 'mocha/logger'
require 'mocha/configuration'
@@ -40,11 +41,11 @@ def unnamed_mock(&block)
end
def mock_impersonating(object, &block)
- add_mock(Mock.new(self, ImpersonatingName.new(object), &block))
+ add_mock(Mock.new(self, ImpersonatingName.new(object), ObjectReceiver.new(object), &block))
end
def mock_impersonating_any_instance_of(klass, &block)
- add_mock(Mock.new(self, ImpersonatingAnyInstanceName.new(klass), &block))
+ add_mock(Mock.new(self, ImpersonatingAnyInstanceName.new(klass), AnyInstanceReceiver.new(klass), &block))
end
def new_state_machine(name)
49 lib/mocha/receivers.rb
View
@@ -0,0 +1,49 @@
+module Mocha
+
+ class ObjectReceiver
+
+ def initialize(object)
+ @object = object
+ end
+
+ def mocks
+ object, mocks = @object, []
+ while object do
+ mocks << object.mocha
+ object = object.is_a?(Class) ? object.superclass : nil
+ end
+ mocks
+ end
+
+ end
+
+ class AnyInstanceReceiver
+
+ def initialize(klass)
+ @klass = klass
+ end
+
+ def mocks
+ klass, mocks = @klass, []
+ while klass do
+ mocks << klass.any_instance.mocha
+ klass = klass.superclass
+ end
+ mocks
+ end
+
+ end
+
+ class DefaultReceiver
+
+ def initialize(mock)
+ @mock = mock
+ end
+
+ def mocks
+ [@mock]
+ end
+
+ end
+
+end
34 test/acceptance/stub_any_instance_method_defined_on_superclass_test.rb
View
@@ -0,0 +1,34 @@
+require File.expand_path('../acceptance_test_helper', __FILE__)
+require 'mocha/setup'
+
+class StubAnyInstanceMethodDefinedOnSuperclassTest < Mocha::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
43 test/acceptance/stub_class_method_defined_on_superclass_test.rb
View
@@ -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
@@ -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
@@ -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
@@ -72,4 +72,41 @@ 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
+
+ def test_stub_on_earliest_receiver_should_take_priority
+ superklass = Class.new do
+ class << self
+ def my_class_method
+ :original_return_value
+ end
+ end
+ end
+ klass = Class.new(superklass)
+ test_result = run_as_test do
+ klass.stubs(:my_class_method).returns(:klass_return_value)
+ superklass.stubs(:my_class_method).returns(:superklass_return_value)
+ assert_equal :klass_return_value, klass.my_class_method
+ end
+ assert_passed(test_result)
+ end
end
11 test/unit/expectation_list_test.rb
View
@@ -68,4 +68,15 @@ def test_should_find_most_recent_matching_expectation_allowing_invocation
assert_same expectation1, expectation_list.match_allowing_invocation(:my_method)
end
+ def test_should_combine_two_expectation_lists_into_one
+ expectation_list1 = ExpectationList.new
+ expectation_list2 = ExpectationList.new
+ expectation1 = Expectation.new(nil, :my_method)
+ expectation2 = Expectation.new(nil, :my_method)
+ expectation_list1.add(expectation1)
+ expectation_list2.add(expectation2)
+ expectation_list = expectation_list1 + expectation_list2
+ assert_equal [expectation1, expectation2], expectation_list.to_a
+ end
+
end
66 test/unit/receivers_test.rb
View
@@ -0,0 +1,66 @@
+require File.expand_path('../../test_helper', __FILE__)
+require 'mocha/receivers'
+
+class ObjectReceiverTest < Mocha::TestCase
+ include Mocha
+
+ class FakeObject < Struct.new(:mocha)
+ def is_a?(klass)
+ false
+ end
+ end
+
+ class FakeClass < Struct.new(:superclass, :mocha)
+ def is_a?(klass)
+ klass == Class
+ end
+ end
+
+ def test_mocks_returns_mock_for_object
+ object = FakeObject.new(:mocha)
+ receiver = ObjectReceiver.new(object)
+ assert_equal [:mocha], receiver.mocks
+ end
+
+ def test_mocks_returns_mocks_for_class_and_its_superclasses
+ grandparent = FakeClass.new(nil, :grandparent_mocha)
+ parent = FakeClass.new(grandparent, :parent_mocha)
+ klass = FakeClass.new(parent, :mocha)
+ receiver = ObjectReceiver.new(klass)
+ assert_equal [:mocha, :parent_mocha, :grandparent_mocha], receiver.mocks
+ end
+end
+
+class AnyInstanceReceiverTest < Mocha::TestCase
+ include Mocha
+
+ class FakeAnyInstanceClass
+ attr_reader :superclass
+
+ def initialize(superclass, mocha)
+ @superclass, @mocha = superclass, mocha
+ end
+
+ def any_instance
+ Struct.new(:mocha).new(@mocha)
+ end
+ end
+
+ def test_mocks_returns_mocks_for_class_and_its_superclasses
+ grandparent = FakeAnyInstanceClass.new(nil, :grandparent_mocha)
+ parent = FakeAnyInstanceClass.new(grandparent, :parent_mocha)
+ klass = FakeAnyInstanceClass.new(parent, :mocha)
+ receiver = AnyInstanceReceiver.new(klass)
+ assert_equal [:mocha, :parent_mocha, :grandparent_mocha], receiver.mocks
+ end
+end
+
+class DefaultReceiverTest < Mocha::TestCase
+ include Mocha
+
+ def test_mocks_returns_mock
+ mock = :mocha
+ receiver = DefaultReceiver.new(mock)
+ assert_equal [:mocha], receiver.mocks
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.