From 22c594ffdc7da329fc6295060e273196e32ff5bd Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Fri, 29 Nov 2019 12:02:25 +0000 Subject: [PATCH 01/34] make arg iteration similar in expects & stubs This will allow us to keep the code DRY by extracting a method with the identical bits --- lib/mocha/mock.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index e72d75453..4eb950faa 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -150,9 +150,9 @@ def stubs(method_name_or_hash, backtrace = nil) method_name = args.shift ensure_method_not_already_defined(method_name) expectation = Expectation.new(self, method_name, backtrace) - expectation.at_least(0) expectation.returns(args.shift) unless args.empty? @expectations.add(expectation) + expectation.at_least(0) end end From ffac2adcb734019c7c83f837218a8e9342a29b3b Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Fri, 29 Nov 2019 12:04:55 +0000 Subject: [PATCH 02/34] Refactor: extract add_expectation to DRY expects & stubs --- lib/mocha/mock.rb | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index 4eb950faa..f737f56a4 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -111,11 +111,7 @@ class Mock def expects(method_name_or_hash, backtrace = nil) iterator = ArgumentIterator.new(method_name_or_hash) iterator.each do |*args| - method_name = args.shift - ensure_method_not_already_defined(method_name) - expectation = Expectation.new(self, method_name, backtrace) - expectation.returns(args.shift) unless args.empty? - @expectations.add(expectation) + add_expectation(args, backtrace) end end @@ -147,11 +143,7 @@ def expects(method_name_or_hash, backtrace = nil) def stubs(method_name_or_hash, backtrace = nil) iterator = ArgumentIterator.new(method_name_or_hash) iterator.each do |*args| - method_name = args.shift - ensure_method_not_already_defined(method_name) - expectation = Expectation.new(self, method_name, backtrace) - expectation.returns(args.shift) unless args.empty? - @expectations.add(expectation) + expectation = add_expectation(args, backtrace) expectation.at_least(0) end end @@ -370,5 +362,16 @@ def ensure_method_not_already_defined(method_name) def any_expectations? @expectations.any? end + + private + + def add_expectation(args, backtrace) + method_name = args.shift + ensure_method_not_already_defined(method_name) + expectation = Expectation.new(self, method_name, backtrace) + expectation.returns(args.shift) unless args.empty? + @expectations.add(expectation) + expectation + end end end From f700c36022ab337e969154814a13990b8af2d3cf Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Fri, 29 Nov 2019 12:06:52 +0000 Subject: [PATCH 03/34] Refactor: inline temp - expectation --- lib/mocha/mock.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index f737f56a4..eaffd7a71 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -143,8 +143,7 @@ def expects(method_name_or_hash, backtrace = nil) def stubs(method_name_or_hash, backtrace = nil) iterator = ArgumentIterator.new(method_name_or_hash) iterator.each do |*args| - expectation = add_expectation(args, backtrace) - expectation.at_least(0) + add_expectation(args, backtrace).at_least(0) end end From da280b1b0333b183aec27e20ffa1630e34aa81b6 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Fri, 29 Nov 2019 12:07:44 +0000 Subject: [PATCH 04/34] Refactor: inline temp - iterator --- lib/mocha/mock.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index eaffd7a71..1091e19ad 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -109,8 +109,7 @@ class Mock # object.expects(:expected_method_one).returns(:result_one) # object.expects(:expected_method_two).returns(:result_two) def expects(method_name_or_hash, backtrace = nil) - iterator = ArgumentIterator.new(method_name_or_hash) - iterator.each do |*args| + ArgumentIterator.new(method_name_or_hash).each do |*args| add_expectation(args, backtrace) end end @@ -141,8 +140,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) - iterator = ArgumentIterator.new(method_name_or_hash) - iterator.each do |*args| + ArgumentIterator.new(method_name_or_hash).each do |*args| add_expectation(args, backtrace).at_least(0) end end From 75794168a36d126750a2bd28ba8366b1111230c8 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Fri, 29 Nov 2019 13:47:45 +0000 Subject: [PATCH 05/34] Refactor: extract anticipates to DRY expects,stubs --- lib/mocha/object_methods.rb | 52 ++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 4e4e59673..18fcd1d56 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -72,21 +72,7 @@ def expects(expected_methods_vs_return_values) if expected_methods_vs_return_values.to_s =~ /the[^a-z]*spanish[^a-z]*inquisition/i raise ExpectationErrorFactory.build('NOBODY EXPECTS THE SPANISH INQUISITION!') end - if frozen? - raise StubbingError.new("can't stub method on frozen object: #{mocha_inspect}", caller) - end - expectation = nil - mockery = Mocha::Mockery.instance - iterator = ArgumentIterator.new(expected_methods_vs_return_values) - iterator.each do |*args| - method_name = args.shift - mockery.on_stubbing(self, method_name) - method = stubba_method.new(stubba_object, method_name) - mockery.stubba.stub(method) - expectation = mocha.expects(method_name, caller) - expectation.returns(args.shift) unless args.empty? - end - expectation + anticipates(expected_methods_vs_return_values) { |method_name, caller| mocha.expects(method_name, caller) } end # Adds an expectation that the specified method may be called any number of times with any parameters. @@ -118,21 +104,7 @@ def expects(expected_methods_vs_return_values) # # @see Mock#stubs def stubs(stubbed_methods_vs_return_values) - if frozen? - raise StubbingError.new("can't stub method on frozen object: #{mocha_inspect}", caller) - end - expectation = nil - mockery = Mocha::Mockery.instance - iterator = ArgumentIterator.new(stubbed_methods_vs_return_values) - iterator.each do |*args| - method_name = args.shift - mockery.on_stubbing(self, method_name) - method = stubba_method.new(stubba_object, method_name) - mockery.stubba.stub(method) - expectation = mocha.stubs(method_name, caller) - expectation.returns(args.shift) unless args.empty? - end - expectation + anticipates(stubbed_methods_vs_return_values) { |method_name, caller| mocha.stubs(method_name, caller) } end # Removes the specified stubbed methods (added by calls to {#expects} or {#stubs}) and all expectations associated with them. @@ -165,5 +137,25 @@ def unstub(*method_names) mockery.stubba.unstub(method) end end + + private + + def anticipates(methods_vs_return_values) + if frozen? + raise StubbingError.new("can't stub method on frozen object: #{mocha_inspect}", caller) + end + expectation = nil + mockery = Mocha::Mockery.instance + iterator = ArgumentIterator.new(methods_vs_return_values) + iterator.each do |*args| + method_name = args.shift + mockery.on_stubbing(self, method_name) + method = stubba_method.new(stubba_object, method_name) + mockery.stubba.stub(method) + expectation = yield method_name, caller + expectation.returns(args.shift) unless args.empty? + end + expectation + end end end From 7cff8fa2d998360c18ecc449ac62dd3fe3a0bedc Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Fri, 29 Nov 2019 15:08:09 +0000 Subject: [PATCH 06/34] Refactor: inline temp - method --- lib/mocha/object_methods.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 18fcd1d56..56ed0f0c0 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -150,8 +150,7 @@ def anticipates(methods_vs_return_values) iterator.each do |*args| method_name = args.shift mockery.on_stubbing(self, method_name) - method = stubba_method.new(stubba_object, method_name) - mockery.stubba.stub(method) + mockery.stubba.stub(stubba_method.new(stubba_object, method_name)) expectation = yield method_name, caller expectation.returns(args.shift) unless args.empty? end From 7494b2d457b2ffa257a65109d14cf25cae8ccb98 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Fri, 29 Nov 2019 15:28:57 +0000 Subject: [PATCH 07/34] Refactor: extract stub_method to be moved to Mockery --- lib/mocha/object_methods.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 56ed0f0c0..39310403e 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -149,12 +149,16 @@ def anticipates(methods_vs_return_values) iterator = ArgumentIterator.new(methods_vs_return_values) iterator.each do |*args| method_name = args.shift - mockery.on_stubbing(self, method_name) - mockery.stubba.stub(stubba_method.new(stubba_object, method_name)) + stub_method(mockery, method_name) expectation = yield method_name, caller expectation.returns(args.shift) unless args.empty? end expectation end + + def stub_method(mockery, method_name) + mockery.on_stubbing(self, method_name) + mockery.stubba.stub(stubba_method.new(stubba_object, method_name)) + end end end From 5f83af590c07a5da24733cdbaed44b724d490984 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Fri, 29 Nov 2019 15:58:02 +0000 Subject: [PATCH 08/34] Refactor: extract & reuse stubbed_method --- lib/mocha/object_methods.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 39310403e..8e5e2352c 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -40,6 +40,11 @@ def stubba_class singleton_class end + # @private + def stubbed_method(method_name) + stubba_method.new(stubba_object, method_name) + end + # Adds an expectation that the specified method must be called exactly once with any parameters. # # The original implementation of the method is replaced during the test and then restored at the end of the test. The temporary replacement method has the same visibility as the original method. @@ -133,8 +138,7 @@ def stubs(stubbed_methods_vs_return_values) def unstub(*method_names) mockery = Mocha::Mockery.instance method_names.each do |method_name| - method = stubba_method.new(stubba_object, method_name) - mockery.stubba.unstub(method) + mockery.stubba.unstub(stubbed_method(method_name)) end end @@ -158,7 +162,7 @@ def anticipates(methods_vs_return_values) def stub_method(mockery, method_name) mockery.on_stubbing(self, method_name) - mockery.stubba.stub(stubba_method.new(stubba_object, method_name)) + mockery.stubba.stub(stubbed_method(method_name)) end end end From b9422bae794f5b563048b4c562eb482873e508f6 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Fri, 29 Nov 2019 16:00:59 +0000 Subject: [PATCH 09/34] Refactor: move stub_method to Mockery from ObjectMethods --- lib/mocha/mockery.rb | 5 +++++ lib/mocha/object_methods.rb | 7 +------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/mocha/mockery.rb b/lib/mocha/mockery.rb index df80a4300..1e2a7cf4b 100644 --- a/lib/mocha/mockery.rb +++ b/lib/mocha/mockery.rb @@ -82,6 +82,11 @@ def new_state_machine(name) add_state_machine(StateMachine.new(name)) end + def stub_method(object, method_name) + on_stubbing(object, method_name) + stubba.stub(object.stubbed_method(method_name)) + end + def verify(assertion_counter = nil) unless mocks.all? { |mock| mock.__verified__?(assertion_counter) } message = "not all expectations were satisfied\n#{mocha_inspect}" diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 8e5e2352c..507fb307d 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -153,16 +153,11 @@ def anticipates(methods_vs_return_values) iterator = ArgumentIterator.new(methods_vs_return_values) iterator.each do |*args| method_name = args.shift - stub_method(mockery, method_name) + mockery.stub_method(self, method_name) expectation = yield method_name, caller expectation.returns(args.shift) unless args.empty? end expectation end - - def stub_method(mockery, method_name) - mockery.on_stubbing(self, method_name) - mockery.stubba.stub(stubbed_method(method_name)) - end end end From 7a691d982b4be20ce16310c08202754340ab6923 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Fri, 29 Nov 2019 16:06:26 +0000 Subject: [PATCH 10/34] Refactor: make on_stubbing{,_method_unnecessarily} private --- lib/mocha/mockery.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/mocha/mockery.rb b/lib/mocha/mockery.rb index 1e2a7cf4b..ada6cf259 100644 --- a/lib/mocha/mockery.rb +++ b/lib/mocha/mockery.rb @@ -130,6 +130,14 @@ def mocha_inspect message end + attr_writer :logger + + def logger + @logger ||= Logger.new($stderr) + end + + private + def on_stubbing(object, method) method = PRE_RUBY_V19 ? method.to_s : method.to_sym method_signature = "#{object.mocha_inspect}.#{method}" @@ -147,14 +155,6 @@ def on_stubbing_method_unnecessarily(expectation) check(:stubbing_method_unnecessarily, 'method unnecessarily', expectation.method_signature, expectation.backtrace) end - attr_writer :logger - - def logger - @logger ||= Logger.new($stderr) - end - - private - def check(action, description, method_signature, backtrace = caller) return if block_given? && !yield message = "stubbing #{description}: #{method_signature}" From bf32420e0a1844a1f735638c5b95e4a855fef80b Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Fri, 29 Nov 2019 16:13:23 +0000 Subject: [PATCH 11/34] Refactor: rename on_stubbingx to check_stubbingx --- lib/mocha/mockery.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/mocha/mockery.rb b/lib/mocha/mockery.rb index ada6cf259..3b37643c9 100644 --- a/lib/mocha/mockery.rb +++ b/lib/mocha/mockery.rb @@ -83,7 +83,7 @@ def new_state_machine(name) end def stub_method(object, method_name) - on_stubbing(object, method_name) + check_stubbing(object, method_name) stubba.stub(object.stubbed_method(method_name)) end @@ -100,7 +100,7 @@ def verify(assertion_counter = nil) expectations.each do |e| unless Mocha.configuration.stubbing_method_unnecessarily == :allow next if e.used? - on_stubbing_method_unnecessarily(e) + check_stubbing_method_unnecessarily(e) end end end @@ -138,7 +138,7 @@ def logger private - def on_stubbing(object, method) + def check_stubbing(object, method) method = PRE_RUBY_V19 ? method.to_s : method.to_sym method_signature = "#{object.mocha_inspect}.#{method}" check(:stubbing_non_existent_method, 'non-existent method', method_signature) do @@ -151,7 +151,7 @@ def on_stubbing(object, method) check(:stubbing_method_on_non_mock_object, 'method on non-mock object', method_signature) end - def on_stubbing_method_unnecessarily(expectation) + def check_stubbing_method_unnecessarily(expectation) check(:stubbing_method_unnecessarily, 'method unnecessarily', expectation.method_signature, expectation.backtrace) end From 73cf9597ba5cd5d5d51c37b9a29318124bb9c260 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Fri, 29 Nov 2019 20:39:07 +0000 Subject: [PATCH 12/34] Refactor: no need for stateful obj for arg iter-n --- lib/mocha/argument_iterator.rb | 12 ++++-------- lib/mocha/mock.rb | 4 ++-- lib/mocha/object_methods.rb | 3 +-- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/lib/mocha/argument_iterator.rb b/lib/mocha/argument_iterator.rb index 8bca04a4c..8d83a8f51 100644 --- a/lib/mocha/argument_iterator.rb +++ b/lib/mocha/argument_iterator.rb @@ -1,16 +1,12 @@ module Mocha class ArgumentIterator - def initialize(argument) - @argument = argument - end - - def each - if @argument.is_a?(Hash) - @argument.each do |method_name, return_value| + def self.each(argument) + if argument.is_a?(Hash) + argument.each do |method_name, return_value| yield method_name, return_value end else - yield @argument + yield argument end end end diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index 1091e19ad..778558ffd 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -109,7 +109,7 @@ class Mock # object.expects(:expected_method_one).returns(:result_one) # object.expects(:expected_method_two).returns(:result_two) def expects(method_name_or_hash, backtrace = nil) - ArgumentIterator.new(method_name_or_hash).each do |*args| + ArgumentIterator.each(method_name_or_hash) do |*args| add_expectation(args, backtrace) end end @@ -140,7 +140,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) - ArgumentIterator.new(method_name_or_hash).each do |*args| + ArgumentIterator.each(method_name_or_hash) do |*args| add_expectation(args, backtrace).at_least(0) end end diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 507fb307d..1e853783c 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -150,8 +150,7 @@ def anticipates(methods_vs_return_values) end expectation = nil mockery = Mocha::Mockery.instance - iterator = ArgumentIterator.new(methods_vs_return_values) - iterator.each do |*args| + ArgumentIterator.each(methods_vs_return_values) do |*args| method_name = args.shift mockery.stub_method(self, method_name) expectation = yield method_name, caller From b34726e09bd83e4267446503e7416dacc0562611 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Sat, 30 Nov 2019 09:55:29 +0000 Subject: [PATCH 13/34] substitute algorithm for ArgumentIterator.each Array will return a single-element array in case the arg is a single value, or a Hash converted to an array in case the arg is a Hash. We then yield the single element or the key value pair, and return the result of the last yield --- lib/mocha/argument_iterator.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/mocha/argument_iterator.rb b/lib/mocha/argument_iterator.rb index 8d83a8f51..37a848f29 100644 --- a/lib/mocha/argument_iterator.rb +++ b/lib/mocha/argument_iterator.rb @@ -1,13 +1,7 @@ module Mocha class ArgumentIterator def self.each(argument) - if argument.is_a?(Hash) - argument.each do |method_name, return_value| - yield method_name, return_value - end - else - yield argument - end + Array(argument).map { |*args| yield *(args.flatten) }.last end end end From 547a4d5bceccf6cd99a5ba642d23d659db9a3f30 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Sat, 30 Nov 2019 10:06:00 +0000 Subject: [PATCH 14/34] Refactor: extract anticipates to DRY expects,stubs --- lib/mocha/mock.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index 778558ffd..8077fd37d 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -109,9 +109,7 @@ class Mock # object.expects(:expected_method_one).returns(:result_one) # object.expects(:expected_method_two).returns(:result_two) def expects(method_name_or_hash, backtrace = nil) - ArgumentIterator.each(method_name_or_hash) do |*args| - add_expectation(args, backtrace) - end + anticipates(method_name_or_hash, backtrace) end # Adds an expectation that the specified method may be called any number of times with any parameters. @@ -140,9 +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) - ArgumentIterator.each(method_name_or_hash) do |*args| - add_expectation(args, backtrace).at_least(0) - end + anticipates(method_name_or_hash, backtrace) { |expectation| expectation.at_least(0) } end # Removes the specified stubbed methods (added by calls to {#expects} or {#stubs}) and all expectations associated with them. @@ -362,6 +358,14 @@ def any_expectations? private + def anticipates(method_name_or_hash, backtrace, &block) + ArgumentIterator.each(method_name_or_hash) do |*args| + expectation = add_expectation(args, backtrace) + block[expectation] if block + expectation + end + end + def add_expectation(args, backtrace) method_name = args.shift ensure_method_not_already_defined(method_name) From bf7ef4e8020f29b0f69b87c11487456f86e0093c Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Sat, 30 Nov 2019 10:10:52 +0000 Subject: [PATCH 15/34] Refactor: inline add_expectation --- lib/mocha/mock.rb | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index 8077fd37d..a0a58dff4 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -360,19 +360,14 @@ def any_expectations? def anticipates(method_name_or_hash, backtrace, &block) ArgumentIterator.each(method_name_or_hash) do |*args| - expectation = add_expectation(args, backtrace) - block[expectation] if block + method_name = args.shift + ensure_method_not_already_defined(method_name) + expectation = Expectation.new(self, method_name, backtrace) + expectation.returns(args.shift) unless args.empty? + @expectations.add(expectation) + yield expectation if block expectation end end - - def add_expectation(args, backtrace) - method_name = args.shift - ensure_method_not_already_defined(method_name) - expectation = Expectation.new(self, method_name, backtrace) - expectation.returns(args.shift) unless args.empty? - @expectations.add(expectation) - expectation - end end end From 84e3ee7de739e43e61c21fda8f16e261fecbccc7 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Sat, 30 Nov 2019 13:18:32 +0000 Subject: [PATCH 16/34] no need to pass caller around --- lib/mocha/object_methods.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 1e853783c..3b1193303 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -77,7 +77,7 @@ def expects(expected_methods_vs_return_values) if expected_methods_vs_return_values.to_s =~ /the[^a-z]*spanish[^a-z]*inquisition/i raise ExpectationErrorFactory.build('NOBODY EXPECTS THE SPANISH INQUISITION!') end - anticipates(expected_methods_vs_return_values) { |method_name, caller| mocha.expects(method_name, caller) } + anticipates(expected_methods_vs_return_values) { |method_name| mocha.expects(method_name, caller) } end # Adds an expectation that the specified method may be called any number of times with any parameters. @@ -109,7 +109,7 @@ def expects(expected_methods_vs_return_values) # # @see Mock#stubs def stubs(stubbed_methods_vs_return_values) - anticipates(stubbed_methods_vs_return_values) { |method_name, caller| mocha.stubs(method_name, caller) } + anticipates(stubbed_methods_vs_return_values) { |method_name| mocha.stubs(method_name, caller) } end # Removes the specified stubbed methods (added by calls to {#expects} or {#stubs}) and all expectations associated with them. @@ -153,7 +153,7 @@ def anticipates(methods_vs_return_values) ArgumentIterator.each(methods_vs_return_values) do |*args| method_name = args.shift mockery.stub_method(self, method_name) - expectation = yield method_name, caller + expectation = yield method_name expectation.returns(args.shift) unless args.empty? end expectation From bfd20d8a1956c38846129ebe17636e366f465d43 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Sat, 30 Nov 2019 13:27:15 +0000 Subject: [PATCH 17/34] ArgumentIterator.each returns the last result anyway --- lib/mocha/object_methods.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 3b1193303..20f7bb1db 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -148,15 +148,14 @@ def anticipates(methods_vs_return_values) if frozen? raise StubbingError.new("can't stub method on frozen object: #{mocha_inspect}", caller) end - expectation = nil mockery = Mocha::Mockery.instance ArgumentIterator.each(methods_vs_return_values) do |*args| method_name = args.shift mockery.stub_method(self, method_name) expectation = yield method_name expectation.returns(args.shift) unless args.empty? + expectation end - expectation end end end From 9761be3d3cfe736f6ce9ac2b348d54cfc1a533b5 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Sat, 30 Nov 2019 13:28:30 +0000 Subject: [PATCH 18/34] Refactor: inline temp - mocker to restrict scope --- lib/mocha/object_methods.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 20f7bb1db..9e861645b 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -148,10 +148,9 @@ def anticipates(methods_vs_return_values) if frozen? raise StubbingError.new("can't stub method on frozen object: #{mocha_inspect}", caller) end - mockery = Mocha::Mockery.instance ArgumentIterator.each(methods_vs_return_values) do |*args| method_name = args.shift - mockery.stub_method(self, method_name) + Mocha::Mockery.instance.stub_method(self, method_name) expectation = yield method_name expectation.returns(args.shift) unless args.empty? expectation From b2354dfb0f9dfb1f2589b64a7b89eb56a9be8b4f Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Sun, 1 Dec 2019 08:29:04 +0000 Subject: [PATCH 19/34] call Mock#{expects,stubs} with methods_vs_return_values Mock#{expects,stubs} already handle methods_vs_return_values/method_name_or_hash Note that this change isn't 100% behavior preserving due to the separation of Mockery#stub_method call from the Mock#{expects,stubs} call. It's possible that midway through argument iteration, stub_method raises a StubbingError, which would leave some methods stubbed without having an expectation set for them. However, I think that's still OK, since you wouldn't expect anything useful to happen after a StubbingError anyway. We're changing undocumented/unpublished behavior in a way that shouldn't matter. --- lib/mocha/object_methods.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 9e861645b..8ed05d180 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -151,10 +151,8 @@ def anticipates(methods_vs_return_values) ArgumentIterator.each(methods_vs_return_values) do |*args| method_name = args.shift Mocha::Mockery.instance.stub_method(self, method_name) - expectation = yield method_name - expectation.returns(args.shift) unless args.empty? - expectation end + yield methods_vs_return_values end end end From 7e7a61bfb34c99f36d1b7c58ed53a7d106d23ca7 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Sun, 1 Dec 2019 08:38:28 +0000 Subject: [PATCH 20/34] Refactor: prep to move by inlining temp - method --- lib/mocha/object_methods.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 8ed05d180..72a910c2c 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -149,8 +149,7 @@ def anticipates(methods_vs_return_values) raise StubbingError.new("can't stub method on frozen object: #{mocha_inspect}", caller) end ArgumentIterator.each(methods_vs_return_values) do |*args| - method_name = args.shift - Mocha::Mockery.instance.stub_method(self, method_name) + Mocha::Mockery.instance.stub_method(self, args.shift) end yield methods_vs_return_values end From b87733f50984f9333caa3c538f995709a106ef76 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Sun, 1 Dec 2019 09:17:19 +0000 Subject: [PATCH 21/34] Refactor: replace ending yield w/ call in sequence --- lib/mocha/object_methods.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 72a910c2c..32cadc3ad 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -77,7 +77,8 @@ def expects(expected_methods_vs_return_values) if expected_methods_vs_return_values.to_s =~ /the[^a-z]*spanish[^a-z]*inquisition/i raise ExpectationErrorFactory.build('NOBODY EXPECTS THE SPANISH INQUISITION!') end - anticipates(expected_methods_vs_return_values) { |method_name| mocha.expects(method_name, caller) } + anticipates(expected_methods_vs_return_values) + mocha.expects(expected_methods_vs_return_values, caller) end # Adds an expectation that the specified method may be called any number of times with any parameters. @@ -109,7 +110,8 @@ def expects(expected_methods_vs_return_values) # # @see Mock#stubs def stubs(stubbed_methods_vs_return_values) - anticipates(stubbed_methods_vs_return_values) { |method_name| mocha.stubs(method_name, caller) } + anticipates(stubbed_methods_vs_return_values) + mocha.stubs(stubbed_methods_vs_return_values, caller) end # Removes the specified stubbed methods (added by calls to {#expects} or {#stubs}) and all expectations associated with them. @@ -151,7 +153,6 @@ def anticipates(methods_vs_return_values) ArgumentIterator.each(methods_vs_return_values) do |*args| Mocha::Mockery.instance.stub_method(self, args.shift) end - yield methods_vs_return_values end end end From 2c4a5dbec8afc3d7b4f7992ee5dc701f8173b052 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Sun, 1 Dec 2019 12:23:58 +0000 Subject: [PATCH 22/34] call stub_method alongside expectation setting for each method --- lib/mocha/mock.rb | 6 +++--- lib/mocha/object_methods.rb | 13 +++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index a0a58dff4..6637d0dfb 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -356,11 +356,11 @@ def any_expectations? @expectations.any? end - private - - def anticipates(method_name_or_hash, backtrace, &block) + # @private + def anticipates(method_name_or_hash, backtrace = nil, object = Mock.new(@mockery), &block) ArgumentIterator.each(method_name_or_hash) do |*args| 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? diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 32cadc3ad..83f50a9e5 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -77,8 +77,8 @@ def expects(expected_methods_vs_return_values) if expected_methods_vs_return_values.to_s =~ /the[^a-z]*spanish[^a-z]*inquisition/i raise ExpectationErrorFactory.build('NOBODY EXPECTS THE SPANISH INQUISITION!') end - anticipates(expected_methods_vs_return_values) - mocha.expects(expected_methods_vs_return_values, caller) + error_if_frozen + mocha.anticipates(expected_methods_vs_return_values, caller, self) end # Adds an expectation that the specified method may be called any number of times with any parameters. @@ -110,8 +110,8 @@ def expects(expected_methods_vs_return_values) # # @see Mock#stubs def stubs(stubbed_methods_vs_return_values) - anticipates(stubbed_methods_vs_return_values) - mocha.stubs(stubbed_methods_vs_return_values, caller) + error_if_frozen + mocha.anticipates(stubbed_methods_vs_return_values, caller, self) { |expectation| expectation.at_least(0) } end # Removes the specified stubbed methods (added by calls to {#expects} or {#stubs}) and all expectations associated with them. @@ -146,13 +146,10 @@ def unstub(*method_names) private - def anticipates(methods_vs_return_values) + def error_if_frozen if frozen? raise StubbingError.new("can't stub method on frozen object: #{mocha_inspect}", caller) end - ArgumentIterator.each(methods_vs_return_values) do |*args| - Mocha::Mockery.instance.stub_method(self, args.shift) - end end end end From 262ea4c6a5bc366bbfc753744ac49095905e2a78 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Sun, 1 Dec 2019 13:48:56 +0000 Subject: [PATCH 23/34] add expectation to expectations after fully set up --- lib/mocha/mock.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index 6637d0dfb..281858de0 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -364,9 +364,8 @@ def anticipates(method_name_or_hash, backtrace = nil, object = Mock.new(@mockery ensure_method_not_already_defined(method_name) expectation = Expectation.new(self, method_name, backtrace) expectation.returns(args.shift) unless args.empty? - @expectations.add(expectation) yield expectation if block - expectation + @expectations.add(expectation) end end end From 5a5ed504d08d86e11f9dec3471452aece125b67c Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Sun, 1 Dec 2019 14:32:23 +0000 Subject: [PATCH 24/34] Refactor: rename stubbed_method->stubba_method_for Keep the stubba_ prefix to avoid accidental name clash with other valid methods --- lib/mocha/mockery.rb | 2 +- lib/mocha/object_methods.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mocha/mockery.rb b/lib/mocha/mockery.rb index 3b37643c9..e2fdc0ae3 100644 --- a/lib/mocha/mockery.rb +++ b/lib/mocha/mockery.rb @@ -84,7 +84,7 @@ def new_state_machine(name) def stub_method(object, method_name) check_stubbing(object, method_name) - stubba.stub(object.stubbed_method(method_name)) + stubba.stub(object.stubba_method_for(method_name)) end def verify(assertion_counter = nil) diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 83f50a9e5..f43bda790 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -41,7 +41,7 @@ def stubba_class end # @private - def stubbed_method(method_name) + def stubba_method_for(method_name) stubba_method.new(stubba_object, method_name) end @@ -140,7 +140,7 @@ def stubs(stubbed_methods_vs_return_values) def unstub(*method_names) mockery = Mocha::Mockery.instance method_names.each do |method_name| - mockery.stubba.unstub(stubbed_method(method_name)) + mockery.stubba.unstub(stubba_method_for(method_name)) end end From 5f1c5f4053ebdde1c822264d3565d0fd11f350e4 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Mon, 2 Dec 2019 08:20:20 +0000 Subject: [PATCH 25/34] Refactor: move ArgumentIterator#each to Mock --- lib/mocha/argument_iterator.rb | 7 ------- lib/mocha/mock.rb | 9 +++++++-- lib/mocha/object_methods.rb | 1 - test/unit/mock_test.rb | 2 ++ 4 files changed, 9 insertions(+), 10 deletions(-) delete mode 100644 lib/mocha/argument_iterator.rb diff --git a/lib/mocha/argument_iterator.rb b/lib/mocha/argument_iterator.rb deleted file mode 100644 index 37a848f29..000000000 --- a/lib/mocha/argument_iterator.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Mocha - class ArgumentIterator - def self.each(argument) - Array(argument).map { |*args| yield *(args.flatten) }.last - end - end -end diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index 281858de0..4da5bb44a 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -6,7 +6,6 @@ require 'mocha/receivers' require 'mocha/method_matcher' require 'mocha/parameters_matcher' -require 'mocha/argument_iterator' require 'mocha/expectation_error_factory' require 'mocha/ruby_version' @@ -358,7 +357,7 @@ def any_expectations? # @private def anticipates(method_name_or_hash, backtrace = nil, object = Mock.new(@mockery), &block) - ArgumentIterator.each(method_name_or_hash) do |*args| + each_argument(method_name_or_hash) do |*args| method_name = args.shift Mockery.instance.stub_method(object, method_name) unless object.is_a?(Mock) ensure_method_not_already_defined(method_name) @@ -368,5 +367,11 @@ def anticipates(method_name_or_hash, backtrace = nil, object = Mock.new(@mockery @expectations.add(expectation) end end + + private + + def each_argument(argument) + Array(argument).map { |*args| yield *(args.flatten) }.last + end end end diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index f43bda790..21de31070 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -1,6 +1,5 @@ require 'mocha/mockery' require 'mocha/instance_method' -require 'mocha/argument_iterator' require 'mocha/expectation_error_factory' module Mocha diff --git a/test/unit/mock_test.rb b/test/unit/mock_test.rb index ed31db5ce..5cf91a883 100644 --- a/test/unit/mock_test.rb +++ b/test/unit/mock_test.rb @@ -49,6 +49,7 @@ def test_should_be_equal method_missing singleton_method_undefined initialize + Array ].freeze MACOS_EXCLUDED_METHODS = @@ -60,6 +61,7 @@ def test_should_be_equal :singleton_method_undefined, :initialize, :String, + :Array, :singleton_method_added, *MACOS_EXCLUDED_METHODS ].freeze From 365884e9453461e6a0c3368dbcf353f6c520893d Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Mon, 2 Dec 2019 08:25:23 +0000 Subject: [PATCH 26/34] Refactor: inline each_argument --- lib/mocha/mock.rb | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index 4da5bb44a..44742c5d4 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -357,7 +357,8 @@ def any_expectations? # @private def anticipates(method_name_or_hash, backtrace = nil, object = Mock.new(@mockery), &block) - each_argument(method_name_or_hash) do |*args| + 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) @@ -365,13 +366,7 @@ def anticipates(method_name_or_hash, backtrace = nil, object = Mock.new(@mockery expectation.returns(args.shift) unless args.empty? yield expectation if block @expectations.add(expectation) - end - end - - private - - def each_argument(argument) - Array(argument).map { |*args| yield *(args.flatten) }.last + end.last end end end From 19004e74a7595e6ecf41444cad7fd61e0c130bbb Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Mon, 2 Dec 2019 08:28:35 +0000 Subject: [PATCH 27/34] Refactor: DRY up {PRE_RUBY_V19,RUBY_V19_AND_LATER}_EXCLUDED_METHODS --- test/unit/mock_test.rb | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/test/unit/mock_test.rb b/test/unit/mock_test.rb index 5cf91a883..020c9b618 100644 --- a/test/unit/mock_test.rb +++ b/test/unit/mock_test.rb @@ -55,16 +55,13 @@ def test_should_be_equal MACOS_EXCLUDED_METHODS = MACOS && MACOS_VERSION >= MACOS_MOJAVE_VERSION ? [:syscall] : [] - RUBY_V19_AND_LATER_EXCLUDED_METHODS = [ - :object_id, - :method_missing, - :singleton_method_undefined, - :initialize, - :String, - :Array, - :singleton_method_added, - *MACOS_EXCLUDED_METHODS - ].freeze + RUBY_V19_AND_LATER_EXCLUDED_METHODS = + (PRE_RUBY_V19_EXCLUDED_METHODS.map(&:to_sym) + [ + :object_id, + :String, + :singleton_method_added, + *MACOS_EXCLUDED_METHODS + ]).freeze OBJECT_METHODS = STANDARD_OBJECT_PUBLIC_INSTANCE_METHODS.reject do |m| (m =~ /^__.*__$/) || From 201cfaa454f6ab39b7568916b7999aac82939442 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Mon, 2 Dec 2019 10:05:50 +0000 Subject: [PATCH 28/34] Refactor: extract anticipates to DRY expects,stubs --- lib/mocha/object_methods.rb | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 21de31070..a3c01d05f 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -76,8 +76,7 @@ def expects(expected_methods_vs_return_values) if expected_methods_vs_return_values.to_s =~ /the[^a-z]*spanish[^a-z]*inquisition/i raise ExpectationErrorFactory.build('NOBODY EXPECTS THE SPANISH INQUISITION!') end - error_if_frozen - mocha.anticipates(expected_methods_vs_return_values, caller, self) + anticipates(expected_methods_vs_return_values) end # Adds an expectation that the specified method may be called any number of times with any parameters. @@ -109,8 +108,7 @@ def expects(expected_methods_vs_return_values) # # @see Mock#stubs def stubs(stubbed_methods_vs_return_values) - error_if_frozen - mocha.anticipates(stubbed_methods_vs_return_values, caller, self) { |expectation| expectation.at_least(0) } + anticipates(stubbed_methods_vs_return_values) { |expectation| expectation.at_least(0) } end # Removes the specified stubbed methods (added by calls to {#expects} or {#stubs}) and all expectations associated with them. @@ -145,6 +143,11 @@ def unstub(*method_names) private + def anticipates(expected_methods_vs_return_values, &block) + error_if_frozen + mocha.anticipates(expected_methods_vs_return_values, caller, self, &block) + end + def error_if_frozen if frozen? raise StubbingError.new("can't stub method on frozen object: #{mocha_inspect}", caller) From 28404119c5a883fcd445ee13ea56bed9a4cc9d75 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Mon, 2 Dec 2019 10:06:27 +0000 Subject: [PATCH 29/34] Refactor: inline method error_if_frozen --- lib/mocha/object_methods.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index a3c01d05f..8625aac03 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -144,14 +144,10 @@ def unstub(*method_names) private def anticipates(expected_methods_vs_return_values, &block) - error_if_frozen - mocha.anticipates(expected_methods_vs_return_values, caller, self, &block) - end - - def error_if_frozen 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) end end end From 583103be2fdfe423c3069ff50604fcfef00ce581 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Wed, 19 Feb 2020 12:54:16 +0000 Subject: [PATCH 30/34] 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 | 18 +++++++++++++ lib/mocha/mock.rb | 10 ++++---- lib/mocha/object_methods.rb | 6 ++--- .../expectations_on_multiple_methods_test.rb | 25 +++++++++++++++++++ test/unit/mock_test.rb | 20 +++------------ 5 files changed, 54 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..ee829ef10 --- /dev/null +++ b/lib/mocha/expectation_setting.rb @@ -0,0 +1,18 @@ +module Mocha + class ExpectationSetting + attr_reader :expectations + + def initialize(expectations) + @expectations = expectations + end + + %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 + ].each do |method_name| + define_method(method_name) do |*args, &block| + ExpectationSetting.new(@expectations.map { |e| e.send(method_name, *args, &block) }) + 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..afb5dc3f2 100644 --- a/test/acceptance/expectations_on_multiple_methods_test.rb +++ b/test/acceptance/expectations_on_multiple_methods_test.rb @@ -52,4 +52,29 @@ 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 [ + "- expected at least twice, invoked once: #{instance.mocha_inspect}.my_instance_method_1(any_parameters)", + "- expected at least twice, invoked once: #{instance.mocha_inspect}.my_instance_method_2(any_parameters)" + ], test_result.failure_message_lines[-2..-1].sort + 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 From b80cb980853b9d79bf8ffd32b17175de6bc241c8 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Thu, 20 Feb 2020 09:22:40 +0000 Subject: [PATCH 31/34] move Mockery#stub_method call to ObjectMethods Mock wasn't the right place for calling it. The new yield mechanism is more general and less ugly, and allocates responsibilities more appropriately. --- lib/mocha/mock.rb | 4 ++-- lib/mocha/object_methods.rb | 4 +++- test/unit/mock_test.rb | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index 46d65367e..37a66bd0a 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -357,11 +357,11 @@ def any_expectations? end # @private - def anticipates(method_name_or_hash, backtrace = nil, object = Mock.new(@mockery)) + def anticipates(method_name_or_hash, backtrace = nil) 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) + yield method_name if block_given? ensure_method_not_already_defined(method_name) expectation = Expectation.new(self, method_name, backtrace) expectation.returns(args.shift) unless args.empty? diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index a2c7ee851..4d5020339 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -147,7 +147,9 @@ 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) + mocha.anticipates(expected_methods_vs_return_values, caller) do |method_name| + Mockery.instance.stub_method(self, method_name) + end end end end diff --git a/test/unit/mock_test.rb b/test/unit/mock_test.rb index 996873788..7a129b086 100644 --- a/test/unit/mock_test.rb +++ b/test/unit/mock_test.rb @@ -50,6 +50,7 @@ def test_should_be_equal singleton_method_undefined initialize Array + block_given? ].freeze MACOS_EXCLUDED_METHODS = From 8a7af66bd4a1685436deda8344a8893bf221bc63 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Fri, 21 Feb 2020 12:33:31 +0000 Subject: [PATCH 32/34] remove the Spanish Inquisition special case Closes #468 and sets us up to reduce the surface area of the internal API exposed to users, by inlining anticipates into expects and using that from stubs --- lib/mocha/object_methods.rb | 3 --- test/unit/object_methods_test.rb | 4 ---- 2 files changed, 7 deletions(-) diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index 4d5020339..cb3e6f8a6 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -73,9 +73,6 @@ def stubba_method_for(method_name) # # @see Mock#expects def expects(expected_methods_vs_return_values) - if expected_methods_vs_return_values.to_s =~ /the[^a-z]*spanish[^a-z]*inquisition/i - raise ExpectationErrorFactory.build('NOBODY EXPECTS THE SPANISH INQUISITION!') - end anticipates(expected_methods_vs_return_values) end diff --git a/test/unit/object_methods_test.rb b/test/unit/object_methods_test.rb index 13b603a07..2abee0c8d 100644 --- a/test/unit/object_methods_test.rb +++ b/test/unit/object_methods_test.rb @@ -51,10 +51,6 @@ def test_should_stub_self_for_object assert_equal @object, @object.stubba_object end - def test_nobody_expects_the_spanish_inquisition - assert_raises(Mocha::ExpectationErrorFactory.exception_class) { @object.expects(:the_spanish_inquisition) } - end - def test_should_alias_object_method klass = Class.new { def self.method_x; end } klass.extend(Mocha::ObjectMethods) From 9930e5abf63a83abf7daefcdac84602029270343 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Fri, 21 Feb 2020 12:38:14 +0000 Subject: [PATCH 33/34] Refactor: inline anticipates into expects This reduces the surface area of the internal API exposed to users as suggested in #467 --- lib/mocha/mock.rb | 25 ++++++++++--------------- lib/mocha/object_methods.rb | 20 +++++++------------- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index 37a66bd0a..850c32ca2 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -109,7 +109,15 @@ class Mock # object.expects(:expected_method_one).returns(:result_one) # object.expects(:expected_method_two).returns(:result_two) def expects(method_name_or_hash, backtrace = nil) - anticipates(method_name_or_hash, backtrace) + ExpectationSetting.new(Array(method_name_or_hash).map do |*args| + args = args.flatten + method_name = args.shift + yield method_name if block_given? + ensure_method_not_already_defined(method_name) + expectation = Expectation.new(self, method_name, backtrace) + expectation.returns(args.shift) unless args.empty? + @expectations.add(expectation) + end) end # Adds an expectation that the specified method may be called any number of times with any parameters. @@ -138,7 +146,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).at_least(0) + expects(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. @@ -355,18 +363,5 @@ def ensure_method_not_already_defined(method_name) def any_expectations? @expectations.any? end - - # @private - def anticipates(method_name_or_hash, backtrace = nil) - ExpectationSetting.new(Array(method_name_or_hash).map do |*args| - args = args.flatten - method_name = args.shift - yield method_name if block_given? - ensure_method_not_already_defined(method_name) - expectation = Expectation.new(self, method_name, backtrace) - expectation.returns(args.shift) unless args.empty? - @expectations.add(expectation) - end) - end end end diff --git a/lib/mocha/object_methods.rb b/lib/mocha/object_methods.rb index cb3e6f8a6..1464552c7 100644 --- a/lib/mocha/object_methods.rb +++ b/lib/mocha/object_methods.rb @@ -73,7 +73,12 @@ def stubba_method_for(method_name) # # @see Mock#expects def expects(expected_methods_vs_return_values) - anticipates(expected_methods_vs_return_values) + if frozen? + raise StubbingError.new("can't stub method on frozen object: #{mocha_inspect}", caller) + end + mocha.expects(expected_methods_vs_return_values, caller) do |method_name| + Mockery.instance.stub_method(self, method_name) + end end # Adds an expectation that the specified method may be called any number of times with any parameters. @@ -105,7 +110,7 @@ def expects(expected_methods_vs_return_values) # # @see Mock#stubs def stubs(stubbed_methods_vs_return_values) - anticipates(stubbed_methods_vs_return_values).at_least(0) + expects(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. @@ -137,16 +142,5 @@ def unstub(*method_names) mockery.stubba.unstub(stubba_method_for(method_name)) end end - - private - - 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) do |method_name| - Mockery.instance.stub_method(self, method_name) - end - end end end From 1906a1036c3ae2c4dce5ed82e627cded49d361e3 Mon Sep 17 00:00:00 2001 From: Nitish Rathi Date: Fri, 21 Feb 2020 15:16:31 +0000 Subject: [PATCH 34/34] Refactor: ExpectationSetting->CompositeExpectation Given that the public API of Expectation consists solely of expectation setting methods, and that the matching, verifying and other misc methods aren't part of the public API, it makes sense to equate the Expectation protocol implicitly to the ExpectationSetting protocol. If we were to segregate interfaces, we might wanna have other internal-facing interfaces like ExpectationMatching, etc. By naming this CompositeExpectation, we don't need (as much) to expose it or to include it in/update the documentation because as far as clients are concerned, they still get back an Expectation (technically, something that quacks like an Expectation). --- .../{expectation_setting.rb => composite_expectation.rb} | 4 ++-- lib/mocha/mock.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename lib/mocha/{expectation_setting.rb => composite_expectation.rb} (75%) diff --git a/lib/mocha/expectation_setting.rb b/lib/mocha/composite_expectation.rb similarity index 75% rename from lib/mocha/expectation_setting.rb rename to lib/mocha/composite_expectation.rb index ee829ef10..d5611111b 100644 --- a/lib/mocha/expectation_setting.rb +++ b/lib/mocha/composite_expectation.rb @@ -1,5 +1,5 @@ module Mocha - class ExpectationSetting + class CompositeExpectation attr_reader :expectations def initialize(expectations) @@ -11,7 +11,7 @@ def initialize(expectations) with yields multiple_yields returns raises throws then when in_sequence ].each do |method_name| define_method(method_name) do |*args, &block| - ExpectationSetting.new(@expectations.map { |e| e.send(method_name, *args, &block) }) + CompositeExpectation.new(@expectations.map { |e| e.send(method_name, *args, &block) }) end end end diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index 850c32ca2..b4b5d35ae 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -1,7 +1,7 @@ require 'mocha/singleton_class' require 'mocha/expectation' require 'mocha/expectation_list' -require 'mocha/expectation_setting' +require 'mocha/composite_expectation' require 'mocha/invocation' require 'mocha/names' require 'mocha/receivers' @@ -109,7 +109,7 @@ class Mock # object.expects(:expected_method_one).returns(:result_one) # object.expects(:expected_method_two).returns(:result_two) def expects(method_name_or_hash, backtrace = nil) - ExpectationSetting.new(Array(method_name_or_hash).map do |*args| + CompositeExpectation.new(Array(method_name_or_hash).map do |*args| args = args.flatten method_name = args.shift yield method_name if block_given?