From de00251566ac0cce89351903b909244e2c6b01be Mon Sep 17 00:00:00 2001 From: James Mead Date: Mon, 15 Aug 2022 15:53:02 +0100 Subject: [PATCH] Implicitly set responder on partial mocks We now automatically set a responder on mock object which are used for partial mocks. Having made the change above, I had to set include_all to true for the call to Object#respond_to? in Mock#check_responder_responds_to in order to fix a load of broken tests. The legacy_behaviour_for_array_flatten condition in Mock#check_responder_responds_to is needed to avoid a regression of #580 in Ruby < v2.3. Hopefully this is a small step towards having Configuration.prevent(:stubbing_non_existent_method) check Method#arity and/or Method#parameters (#149) and rationalising Configuration.stubbing_non_existent_method= & Mock#responds_like (#531). --- lib/mocha/class_methods.rb | 2 +- lib/mocha/mock.rb | 11 ++++++++--- lib/mocha/object_methods.rb | 2 +- lib/mocha/ruby_version.rb | 1 + test/acceptance/responds_like_test.rb | 16 ++++++++-------- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/mocha/class_methods.rb b/lib/mocha/class_methods.rb index 2ccd58fd3..7b2fc32fe 100644 --- a/lib/mocha/class_methods.rb +++ b/lib/mocha/class_methods.rb @@ -12,7 +12,7 @@ def initialize(klass) def mocha(instantiate = true) if instantiate - @mocha ||= Mocha::Mockery.instance.mock_impersonating_any_instance_of(@stubba_object) + @mocha ||= Mocha::Mockery.instance.mock_impersonating_any_instance_of(@stubba_object).responds_like_instance_of(@stubba_object) else defined?(@mocha) ? @mocha : nil end diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index b448cfe00..e6e7e8738 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -8,6 +8,7 @@ require 'mocha/parameters_matcher' require 'mocha/argument_iterator' require 'mocha/expectation_error_factory' +require 'mocha/ruby_version' module Mocha # Traditional mock object. @@ -381,9 +382,13 @@ def raise_unexpected_invocation_error(invocation, matching_expectation) end def check_responder_responds_to(symbol) - if @responder && !@responder.respond_to?(symbol) # rubocop:disable Style/GuardClause - raise NoMethodError, "undefined method `#{symbol}' for #{mocha_inspect} which responds like #{@responder.mocha_inspect}" - end + return unless @responder + + legacy_behaviour_for_array_flatten = !RUBY_V23_PLUS && !@responder.respond_to?(symbol) && (symbol == :to_ary) + + return if @responder.respond_to?(symbol, true) && !legacy_behaviour_for_array_flatten + + raise NoMethodError, "undefined method `#{symbol}' for #{mocha_inspect} which responds like #{@responder.mocha_inspect}" end def check_expiry diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 4e4e59673..045a33174 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -14,7 +14,7 @@ module ObjectMethods # @private def mocha(instantiate = true) if instantiate - @mocha ||= Mocha::Mockery.instance.mock_impersonating(self) + @mocha ||= Mocha::Mockery.instance.mock_impersonating(self).responds_like(self) else defined?(@mocha) ? @mocha : nil end diff --git a/lib/mocha/ruby_version.rb b/lib/mocha/ruby_version.rb index 124eb4729..3d8e857cf 100644 --- a/lib/mocha/ruby_version.rb +++ b/lib/mocha/ruby_version.rb @@ -1,3 +1,4 @@ module Mocha + RUBY_V23_PLUS = Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.3') RUBY_V27_PLUS = Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.7') end diff --git a/test/acceptance/responds_like_test.rb b/test/acceptance/responds_like_test.rb index 06fcc9cbd..1ccbcab88 100644 --- a/test/acceptance/responds_like_test.rb +++ b/test/acceptance/responds_like_test.rb @@ -131,14 +131,14 @@ def foo; end assert_passed(test_result) end - def test_mock_which_responds_like_object_with_protected_method_raises_no_method_error_when_method_is_not_stubbed + def test_mock_which_responds_like_object_with_protected_method_raises_unexpected_invocation_exception_when_method_is_not_stubbed object = Class.new do def foo; end protected :foo end.new test_result = run_as_test do m = mock.responds_like(object) - assert_raises(NoMethodError) { m.foo } # vs Minitest::Assertion for public method + assert_raises(Minitest::Assertion) { m.foo } end assert_passed(test_result) end @@ -169,7 +169,7 @@ def foo; end assert_passed(test_result) end - def test_mock_which_responds_like_object_with_protected_method_raises_no_method_error_when_method_is_stubbed + def test_mock_which_responds_like_object_with_protected_method_does_not_raise_exception_when_method_is_stubbed object = Class.new do def foo; end protected :foo @@ -177,7 +177,7 @@ def foo; end test_result = run_as_test do m = mock.responds_like(object) m.stubs(:foo) - assert_raises(NoMethodError) { m.foo } # vs no exception for public method + assert_nil m.foo end assert_passed(test_result) end @@ -197,14 +197,14 @@ def foo; end assert_passed(test_result) end - def test_mock_which_responds_like_object_with_private_method_raises_no_method_error_when_method_is_not_stubbed + def test_mock_which_responds_like_object_with_private_method_raises_unexpected_invocation_exception_when_method_is_not_stubbed object = Class.new do def foo; end private :foo end.new test_result = run_as_test do m = mock.responds_like(object) - assert_raises(NoMethodError) { m.foo } # vs Minitest::Assertion for public method + assert_raises(Minitest::Assertion) { m.foo } end assert_passed(test_result) end @@ -235,7 +235,7 @@ def foo; end assert_passed(test_result) end - def test_mock_which_responds_like_object_with_private_method_raises_no_method_error_when_method_is_stubbed + def test_mock_which_responds_like_object_with_private_method_does_not_raise_exception_when_method_is_stubbed object = Class.new do def foo; end private :foo @@ -243,7 +243,7 @@ def foo; end test_result = run_as_test do m = mock.responds_like(object) m.stubs(:foo) - assert_raises(NoMethodError) { m.foo } # vs no exception for public method + assert_nil m.foo end assert_passed(test_result) end