From 4214427e7965ba031bcf7aab70921ad1cb99c22c Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Wed, 19 Feb 2020 12:54:16 +0000 Subject: [PATCH] more consistent setting of multiple expectations When calling expects/stubs with a hash (method_names_vs_return_values), only the last expectation would get returned. Any further 'expectation setting' methods chained to that call would, therefore, get called only on the last expectation. This seems arbitrary, and neither evident nor intuitive. We now 'extract' the expectation setting methods into a _virtual_ interface, 'implemented' by both Expectation and ExpectationSetting, and return ExpectationSetting instead of Expectation from the expects/stubs methods. This allows us to pass any further expectation setting method calls on to _each_ of the multiple expectations, rather than just the _last_ (or some other arbitrary) single expectation. --- lib/mocha/expectation_setting.rb | 26 ++++++++++++++++++ lib/mocha/mock.rb | 10 +++---- lib/mocha/object_methods.rb | 6 ++--- .../expectations_on_multiple_methods_test.rb | 27 +++++++++++++++++++ test/unit/mock_test.rb | 20 +++----------- 5 files changed, 64 insertions(+), 25 deletions(-) create mode 100644 lib/mocha/expectation_setting.rb diff --git a/lib/mocha/expectation_setting.rb b/lib/mocha/expectation_setting.rb new file mode 100644 index 000000000..90e6e18aa --- /dev/null +++ b/lib/mocha/expectation_setting.rb @@ -0,0 +1,26 @@ +module Mocha + class ExpectationSetting + EXPECTATION_SETTING_METHODS = %w[ + times twice once never at_least at_least_once at_most at_most_once + with yields multiple_yields returns raises throws then when in_sequence + ].map(&:to_sym).freeze + + attr_reader :expectations + + def initialize(expectations) + @expectations = expectations + end + + def respond_to_missing?(symbol, _include_all = false) + EXPECTATION_SETTING_METHODS.include?(symbol) || super + end + + def method_missing(symbol, *arguments, &block) + if EXPECTATION_SETTING_METHODS.include?(symbol) + ExpectationSetting.new(@expectations.map { |e| e.send(symbol, *arguments, &block) }) + else + super + end + end + end +end diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index 44742c5d4..46d65367e 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -1,6 +1,7 @@ require 'mocha/singleton_class' require 'mocha/expectation' require 'mocha/expectation_list' +require 'mocha/expectation_setting' require 'mocha/invocation' require 'mocha/names' require 'mocha/receivers' @@ -137,7 +138,7 @@ def expects(method_name_or_hash, backtrace = nil) # object.stubs(:stubbed_method_one).returns(:result_one) # object.stubs(:stubbed_method_two).returns(:result_two) def stubs(method_name_or_hash, backtrace = nil) - anticipates(method_name_or_hash, backtrace) { |expectation| expectation.at_least(0) } + anticipates(method_name_or_hash, backtrace).at_least(0) end # Removes the specified stubbed methods (added by calls to {#expects} or {#stubs}) and all expectations associated with them. @@ -356,17 +357,16 @@ def any_expectations? end # @private - def anticipates(method_name_or_hash, backtrace = nil, object = Mock.new(@mockery), &block) - Array(method_name_or_hash).map do |*args| + def anticipates(method_name_or_hash, backtrace = nil, object = Mock.new(@mockery)) + ExpectationSetting.new(Array(method_name_or_hash).map do |*args| args = args.flatten method_name = args.shift Mockery.instance.stub_method(object, method_name) unless object.is_a?(Mock) ensure_method_not_already_defined(method_name) expectation = Expectation.new(self, method_name, backtrace) expectation.returns(args.shift) unless args.empty? - yield expectation if block @expectations.add(expectation) - end.last + end) end end end diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 8625aac03..a2c7ee851 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -108,7 +108,7 @@ def expects(expected_methods_vs_return_values) # # @see Mock#stubs def stubs(stubbed_methods_vs_return_values) - anticipates(stubbed_methods_vs_return_values) { |expectation| expectation.at_least(0) } + anticipates(stubbed_methods_vs_return_values).at_least(0) end # Removes the specified stubbed methods (added by calls to {#expects} or {#stubs}) and all expectations associated with them. @@ -143,11 +143,11 @@ def unstub(*method_names) private - def anticipates(expected_methods_vs_return_values, &block) + def anticipates(expected_methods_vs_return_values) if frozen? raise StubbingError.new("can't stub method on frozen object: #{mocha_inspect}", caller) end - mocha.anticipates(expected_methods_vs_return_values, caller, self, &block) + mocha.anticipates(expected_methods_vs_return_values, caller, self) end end end diff --git a/test/acceptance/expectations_on_multiple_methods_test.rb b/test/acceptance/expectations_on_multiple_methods_test.rb index fe41f5218..182b6a6d8 100644 --- a/test/acceptance/expectations_on_multiple_methods_test.rb +++ b/test/acceptance/expectations_on_multiple_methods_test.rb @@ -52,4 +52,31 @@ def my_instance_method_2 end assert_passed(test_result) end + + def test_should_configure_expectations_for_multiple_methods + instance = Class.new do + def my_instance_method_1 + :original_return_value_1 + end + + def my_instance_method_2 + :original_return_value_2 + end + end.new + test_result = run_as_test do + instance.stubs( + :my_instance_method_1 => :new_return_value_1, + :my_instance_method_2 => :new_return_value_2 + ).at_least(2) + assert_equal :new_return_value_1, instance.my_instance_method_1 + assert_equal :new_return_value_2, instance.my_instance_method_2 + end + assert_failed(test_result) + assert_equal [ + 'not all expectations were satisfied', + 'unsatisfied expectations:', + "- expected at least twice, invoked once: #{instance.mocha_inspect}.my_instance_method_2(any_parameters)", + "- expected at least twice, invoked once: #{instance.mocha_inspect}.my_instance_method_1(any_parameters)" + ], test_result.failure_message_lines + end end diff --git a/test/unit/mock_test.rb b/test/unit/mock_test.rb index 020c9b618..996873788 100644 --- a/test/unit/mock_test.rb +++ b/test/unit/mock_test.rb @@ -21,7 +21,7 @@ def test_should_build_and_store_expectations mock = build_mock expectation = mock.expects(:method1) assert_not_nil expectation - assert_equal [expectation], mock.__expectations__.to_a + assert_equal expectation.expectations, mock.__expectations__.to_a end def test_should_not_stub_everything_by_default @@ -86,28 +86,14 @@ def test_should_create_and_add_expectations mock = build_mock expectation1 = mock.expects(:method1) expectation2 = mock.expects(:method2) - assert_equal [expectation1, expectation2].to_set, mock.__expectations__.to_set - end - - def test_should_pass_backtrace_into_expectation - mock = build_mock - backtrace = Object.new - expectation = mock.expects(:method1, backtrace) - assert_equal backtrace, expectation.backtrace - end - - def test_should_pass_backtrace_into_stub - mock = build_mock - backtrace = Object.new - stub = mock.stubs(:method1, backtrace) - assert_equal backtrace, stub.backtrace + assert_equal (expectation1.expectations + expectation2.expectations).to_set, mock.__expectations__.to_set end def test_should_create_and_add_stubs mock = build_mock stub1 = mock.stubs(:method1) stub2 = mock.stubs(:method2) - assert_equal [stub1, stub2].to_set, mock.__expectations__.to_set + assert_equal (stub1.expectations + stub2.expectations).to_set, mock.__expectations__.to_set end def test_should_invoke_expectation_and_return_result