From 944750b1cf79821fde289cea8106825702928fa3 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Thu, 18 Sep 2014 23:32:09 -0700 Subject: [PATCH 1/6] Treat `any_args` as an arg splat. This allows it to match an arbitrary number of arguments at any point in an arg list. Closes #707. --- Changelog.md | 5 ++ features/basics/expecting_messages.feature | 2 +- features/basics/spies.feature | 8 +- features/old_syntax/should_receive.feature | 2 +- features/outside_rspec/minitest.feature | 26 +++---- features/outside_rspec/standalone.feature | 8 +- lib/rspec/mocks/argument_list_matcher.rb | 32 ++++++-- lib/rspec/mocks/argument_matchers.rb | 2 +- spec/rspec/mocks/argument_matchers_spec.rb | 91 +++++++++++++++++++--- spec/rspec/mocks/partial_double_spec.rb | 2 +- 10 files changed, 137 insertions(+), 41 deletions(-) diff --git a/Changelog.md b/Changelog.md index 2befa90cb..a29cd0469 100644 --- a/Changelog.md +++ b/Changelog.md @@ -15,6 +15,11 @@ Bug Fixes: `have_received` matcher (they are not intended to be used together and previously caused an odd internal failure in rspec-mocks). (Jon Rowe, #788). +Enhancements: + +* Treat `any_args` as an arg splat, allowing it to match an arbitrary + number of args at any point in an arg list. (Myron Marston, #786) + ### 3.1.1 / 2014-09-18 [Full Changelog](http://github.com/rspec/rspec-mocks/compare/v3.1.0...v3.1.1) diff --git a/features/basics/expecting_messages.feature b/features/basics/expecting_messages.feature index f0ff91c02..e885f9509 100644 --- a/features/basics/expecting_messages.feature +++ b/features/basics/expecting_messages.feature @@ -19,7 +19,7 @@ Feature: Expecting messages """ 1) An unfulfilled positive message expectation triggers a failure Failure/Error: expect(dbl).to receive(:foo) - (Double "Some Collaborator").foo(any args) + (Double "Some Collaborator").foo(*(any args)) expected: 1 time with any arguments received: 0 times with any arguments """ diff --git a/features/basics/spies.feature b/features/basics/spies.feature index 195362a04..275333f72 100644 --- a/features/basics/spies.feature +++ b/features/basics/spies.feature @@ -70,7 +70,7 @@ Feature: Spies """ 1) failure when the message has not been received for a spy Failure/Error: expect(invitation).to have_received(:deliver) - (Double "invitation").deliver(any args) + (Double "invitation").deliver(*(any args)) expected: 1 time with any arguments received: 0 times with any arguments """ @@ -78,7 +78,7 @@ Feature: Spies """ 2) failure when the message has not been received for a partial double Failure/Error: expect(Invitation).to have_received(:deliver) - ().deliver(any args) + ().deliver(*(any args)) expected: 1 time with any arguments received: 0 times with any arguments """ @@ -119,7 +119,7 @@ Feature: Spies | | | 1) An invitiation fails when a count constraint is not satisfied | | Failure/Error: expect(invitation).to have_received(:deliver).at_least(3).times | - | (Double "invitation").deliver(any args) | + | (Double "invitation").deliver(*(any args)) | | expected: at least 3 times with any arguments | | received: 2 times with any arguments | | | @@ -140,5 +140,5 @@ Feature: Spies Then it should pass with: """ An invitation - should have received deliver(any args) 1 time + should have received deliver(*(any args)) 1 time """ diff --git a/features/old_syntax/should_receive.feature b/features/old_syntax/should_receive.feature index 1ded4d97b..5e7b4d95d 100644 --- a/features/old_syntax/should_receive.feature +++ b/features/old_syntax/should_receive.feature @@ -36,7 +36,7 @@ Feature: `should_receive` """ 1) An unfulfilled message expectation triggers a failure Failure/Error: dbl.should_receive(:foo) - (Double "Some Collaborator").foo(any args) + (Double "Some Collaborator").foo(*(any args)) expected: 1 time with any arguments received: 0 times with any arguments """ diff --git a/features/outside_rspec/minitest.feature b/features/outside_rspec/minitest.feature index 5520db60c..8e43bd779 100644 --- a/features/outside_rspec/minitest.feature +++ b/features/outside_rspec/minitest.feature @@ -65,16 +65,16 @@ Feature: Integrate with Minitest """ When I run `ruby -Itest test/rspec_mocks_test.rb` Then it should fail with the following output: - | 1) Error: | - | RSpecMocksTest#test_failing_negative_expectation: | - | RSpec::Mocks::MockExpectationError: (Double).message(no args) | - | expected: 0 times with any arguments | - | received: 1 time | - | | - | 2) Error: | - | RSpecMocksTest#test_failing_positive_expectation: | - | RSpec::Mocks::MockExpectationError: (Double).message(any args) | - | expected: 1 time with any arguments | - | received: 0 times with any arguments | - | | - | 4 runs, 0 assertions, 0 failures, 2 errors, 0 skips | + | 1) Error: | + | RSpecMocksTest#test_failing_negative_expectation: | + | RSpec::Mocks::MockExpectationError: (Double).message(no args) | + | expected: 0 times with any arguments | + | received: 1 time | + | | + | 2) Error: | + | RSpecMocksTest#test_failing_positive_expectation: | + | RSpec::Mocks::MockExpectationError: (Double).message(*(any args)) | + | expected: 1 time with any arguments | + | received: 0 times with any arguments | + | | + | 4 runs, 0 assertions, 0 failures, 2 errors, 0 skips | diff --git a/features/outside_rspec/standalone.feature b/features/outside_rspec/standalone.feature index 679c9e3d3..c324e27d3 100644 --- a/features/outside_rspec/standalone.feature +++ b/features/outside_rspec/standalone.feature @@ -27,7 +27,7 @@ Feature: Standalone """ When I run `ruby example.rb` Then it should fail with the following output: - | (Double "greeter").say_hi(any args) | - | RSpec::Mocks::MockExpectationError | - | expected: 1 time with any arguments | - | received: 0 times with any arguments | + | (Double "greeter").say_hi(*(any args)) | + | RSpec::Mocks::MockExpectationError | + | expected: 1 time with any arguments | + | received: 0 times with any arguments | diff --git a/lib/rspec/mocks/argument_list_matcher.rb b/lib/rspec/mocks/argument_list_matcher.rb index a7d15ea5e..7d4a389e2 100644 --- a/lib/rspec/mocks/argument_list_matcher.rb +++ b/lib/rspec/mocks/argument_list_matcher.rb @@ -44,12 +44,6 @@ class ArgumentListMatcher # @see #args_match? def initialize(*expected_args) @expected_args = expected_args - - @matchers = case expected_args.first - when ArgumentMatchers::AnyArgsMatcher then Array - when ArgumentMatchers::NoArgsMatcher then [] - else expected_args - end end # @api public @@ -60,13 +54,37 @@ def initialize(*expected_args) # # @see #initialize def args_match?(*args) - Support::FuzzyMatcher.values_match?(@matchers, args) + Support::FuzzyMatcher.values_match?(matchers_for(args), args) end # Value that will match all argument lists. # # @private MATCH_ALL = new(ArgumentMatchers::AnyArgsMatcher.new) + + # Singleton instance of AnyArgMatcher to save on memory. + # It's immutable and thus safe to re-use many times. + # @private + ANYTHING = ArgumentMatchers::AnyArgMatcher.new + + private + + def matchers_for(actual_args) + return [] if expected_args.one? && ArgumentMatchers::NoArgsMatcher === expected_args.first + + any_args_index = expected_args.index { |arg| ArgumentMatchers::AnyArgsMatcher === arg } + return expected_args unless any_args_index + + replace_any_args_with_splat_of_anything(any_args_index, actual_args.count) + end + + def replace_any_args_with_splat_of_anything(before_count, actual_args_count) + any_args_count = actual_args_count - expected_args.count + 1 + after_count = expected_args.count - before_count - 1 + + any_args = 1.upto(any_args_count).map { ANYTHING } + expected_args.first(before_count) + any_args + expected_args.last(after_count) + end end end end diff --git a/lib/rspec/mocks/argument_matchers.rb b/lib/rspec/mocks/argument_matchers.rb index 9194e2939..e34ea0965 100644 --- a/lib/rspec/mocks/argument_matchers.rb +++ b/lib/rspec/mocks/argument_matchers.rb @@ -129,7 +129,7 @@ def self.anythingize_lonely_keys(*args) # @private class AnyArgsMatcher def description - "any args" + "*(any args)" end end diff --git a/spec/rspec/mocks/argument_matchers_spec.rb b/spec/rspec/mocks/argument_matchers_spec.rb index c8b766ca1..6ecac474a 100644 --- a/spec/rspec/mocks/argument_matchers_spec.rb +++ b/spec/rspec/mocks/argument_matchers_spec.rb @@ -129,19 +129,92 @@ module Mocks end describe "any_args" do - it "matches no args against any_args" do - expect(a_double).to receive(:random_call).with(any_args) - a_double.random_call + context "as the only arg passed to `with`" do + before { expect(a_double).to receive(:random_call).with(any_args) } + + it "matches no args" do + a_double.random_call + end + + it "matches one arg" do + a_double.random_call("a string") + end + + it "matches many args" do + a_double.random_call("a string", :other, 3) + end end - it "matches one arg against any_args" do - expect(a_double).to receive(:random_call).with(any_args) - a_double.random_call("a string") + context "as the last of three args" do + before { expect(a_double).to receive(:random_call).with(1, /foo/, any_args) } + + it "matches a call of two args when it matches the first two explicit args" do + a_double.random_call(1, "food") + end + + it "matches a call of three args when it matches the first two explicit args" do + a_double.random_call(1, "food", :more) + end + + it "matches a call of four args when it matches the first two explicit args" do + a_double.random_call(1, "food", :more, :args) + end + + it "does not match a call where the first two args do not match", :reset => true do + expect { a_double.random_call(1, "bar", 2, 3) }.to fail_matching "expected: (1, /foo/, *(any args))" + end + + it "does not match a call of no args", :reset => true do + expect { a_double.random_call }.to fail_matching "expected: (1, /foo/, *(any args))" + end end - it "handles non matching instances nicely", :reset => true do - expect(a_double).to receive(:random_call).with(1, any_args) - expect { a_double.random_call }.to fail_matching "expected: (1, any args)" + context "as the first of three args" do + before { expect(a_double).to receive(:random_call).with(any_args, 1, /foo/) } + + it "matches a call of two args when it matches the last two explicit args" do + a_double.random_call(1, "food") + end + + it "matches a call of three args when it matches the last two explicit args" do + a_double.random_call(nil, 1, "food") + end + + it "matches a call of four args when it matches the last two explicit args" do + a_double.random_call(:some, :args, 1, "food") + end + + it "does not match a call where the last two args do not match", :reset => true do + expect { a_double.random_call(1, "bar", 2, 3) }.to fail_matching "expected: (*(any args), 1, /foo/)" + end + + it "does not match a call of no args", :reset => true do + expect { a_double.random_call }.to fail_matching "expected: (*(any args), 1, /foo/)" + end + end + + context "as the middle of three args" do + before { expect(a_double).to receive(:random_call).with(1, any_args, /foo/) } + + it "matches a call of two args when it matches the first and last args" do + a_double.random_call(1, "food") + end + + it "matches a call of three args when it matches the first and last args" do + a_double.random_call(1, nil, "food") + end + + it "matches a call of four args when it matches the first and last args" do + a_double.random_call(1, :some, :args, "food") + end + + it "does not match a call where the first and last args do not match", :reset => true do + expect { a_double.random_call(nil, "bar", 2, 3) }.to fail_matching "expected: (1, *(any args), /foo/)" + end + + it "does not match a call of no args", :reset => true do + expect { a_double.random_call }.to fail_matching "expected: (1, *(any args), /foo/)" + end end end diff --git a/spec/rspec/mocks/partial_double_spec.rb b/spec/rspec/mocks/partial_double_spec.rb index 57b2ad55c..46f7d526f 100644 --- a/spec/rspec/mocks/partial_double_spec.rb +++ b/spec/rspec/mocks/partial_double_spec.rb @@ -102,7 +102,7 @@ module Mocks verify _nil }.to raise_error( RSpec::Mocks::MockExpectationError, - %Q|(nil).foobar(any args)\n expected: 1 time with any arguments\n received: 0 times with any arguments| + %Q|(nil).foobar(*(any args))\n expected: 1 time with any arguments\n received: 0 times with any arguments| ) end From 903ebe5ebf2c8bcf1f738dec84316bb62b195cd1 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 20 Sep 2014 13:57:31 -0700 Subject: [PATCH 2/6] Convert stateless arg matchers to singletons. This saves on memory (fewer objects to GC!). --- lib/rspec/mocks/argument_list_matcher.rb | 13 +++------ lib/rspec/mocks/argument_matchers.rb | 36 ++++++++++++++++++------ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/lib/rspec/mocks/argument_list_matcher.rb b/lib/rspec/mocks/argument_list_matcher.rb index 7d4a389e2..fd72dca9d 100644 --- a/lib/rspec/mocks/argument_list_matcher.rb +++ b/lib/rspec/mocks/argument_list_matcher.rb @@ -60,19 +60,14 @@ def args_match?(*args) # Value that will match all argument lists. # # @private - MATCH_ALL = new(ArgumentMatchers::AnyArgsMatcher.new) - - # Singleton instance of AnyArgMatcher to save on memory. - # It's immutable and thus safe to re-use many times. - # @private - ANYTHING = ArgumentMatchers::AnyArgMatcher.new + MATCH_ALL = new(ArgumentMatchers::AnyArgsMatcher::INSTANCE) private def matchers_for(actual_args) - return [] if expected_args.one? && ArgumentMatchers::NoArgsMatcher === expected_args.first + return [] if [ArgumentMatchers::NoArgsMatcher::INSTANCE] == expected_args - any_args_index = expected_args.index { |arg| ArgumentMatchers::AnyArgsMatcher === arg } + any_args_index = expected_args.index(ArgumentMatchers::AnyArgsMatcher::INSTANCE) return expected_args unless any_args_index replace_any_args_with_splat_of_anything(any_args_index, actual_args.count) @@ -82,7 +77,7 @@ def replace_any_args_with_splat_of_anything(before_count, actual_args_count) any_args_count = actual_args_count - expected_args.count + 1 after_count = expected_args.count - before_count - 1 - any_args = 1.upto(any_args_count).map { ANYTHING } + any_args = 1.upto(any_args_count).map { ArgumentMatchers::AnyArgMatcher::INSTANCE } expected_args.first(before_count) + any_args + expected_args.last(after_count) end end diff --git a/lib/rspec/mocks/argument_matchers.rb b/lib/rspec/mocks/argument_matchers.rb index e34ea0965..7e08054f6 100644 --- a/lib/rspec/mocks/argument_matchers.rb +++ b/lib/rspec/mocks/argument_matchers.rb @@ -21,7 +21,7 @@ module ArgumentMatchers # # expect(object).to receive(:message).with(any_args) def any_args - AnyArgsMatcher.new + AnyArgsMatcher::INSTANCE end # Matches any argument at all. @@ -30,7 +30,7 @@ def any_args # # expect(object).to receive(:message).with(anything) def anything - AnyArgMatcher.new + AnyArgMatcher::INSTANCE end # Matches no arguments. @@ -39,7 +39,7 @@ def anything # # expect(object).to receive(:message).with(no_args) def no_args - NoArgsMatcher.new + NoArgsMatcher::INSTANCE end # Matches if the actual argument responds to the specified messages. @@ -58,7 +58,7 @@ def duck_type(*args) # # expect(object).to receive(:message).with(boolean()) def boolean - BooleanMatcher.new + BooleanMatcher::INSTANCE end # Matches a hash that includes the specified key(s) or key/value pairs. @@ -122,19 +122,37 @@ def kind_of(klass) # @private def self.anythingize_lonely_keys(*args) hash = args.last.class == Hash ? args.delete_at(-1) : {} - args.each { | arg | hash[arg] = AnyArgMatcher.new } + args.each { | arg | hash[arg] = AnyArgMatcher::INSTANCE } hash end + # Intended to be subclassed by stateless, immutable argument matchers. + # Provides a `::INSTANCE` constant for accessing a + # global singleton instance of the matcher. This saves on memory a bit + # (as their is no need to construct multiple instance since there is + # no internal instance state). It also facilities the special case logic + # we need for some of these matchers, by making it easy to do comparisons + # like: `[klass::INSTANCE] == args` rather than + # `args.count == 1 && klass === args.first`. + # + # @private + class SingletonMatcher + private_class_method :new + + def self.inherited(subklass) + subklass.const_set(:INSTANCE, subklass.send(:new)) + end + end + # @private - class AnyArgsMatcher + class AnyArgsMatcher < SingletonMatcher def description "*(any args)" end end # @private - class AnyArgMatcher + class AnyArgMatcher < SingletonMatcher def ===(_other) true end @@ -145,14 +163,14 @@ def description end # @private - class NoArgsMatcher + class NoArgsMatcher < SingletonMatcher def description "no args" end end # @private - class BooleanMatcher + class BooleanMatcher < SingletonMatcher def ===(value) true == value || false == value end From 80f6f622800ecebb39836b2e6feda7c07d6286ad Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sat, 20 Sep 2014 21:24:18 -0700 Subject: [PATCH 3/6] Provide a clear failure when invalid args are passed to `with`. --- Changelog.md | 9 +++++++++ lib/rspec/mocks/argument_list_matcher.rb | 22 +++++++++++++++++----- spec/rspec/mocks/argument_matchers_spec.rb | 16 ++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/Changelog.md b/Changelog.md index a29cd0469..b6ebe466d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -20,6 +20,15 @@ Enhancements: * Treat `any_args` as an arg splat, allowing it to match an arbitrary number of args at any point in an arg list. (Myron Marston, #786) +Bug Fixes: + +* Provide a clear error when users wrongly combine `no_args` with + additional arguments (e.g. `expect().to receive().with(no_args, 1)`). + (Myron Marston, #786) +* Provide a clear error when users wrongly use `any_args` multiple times in the + same argument list (e.g. `expect().to receive().with(any_args, 1, any_args)`. + (Myron Marston, #786) + ### 3.1.1 / 2014-09-18 [Full Changelog](http://github.com/rspec/rspec-mocks/compare/v3.1.0...v3.1.1) diff --git a/lib/rspec/mocks/argument_list_matcher.rb b/lib/rspec/mocks/argument_list_matcher.rb index fd72dca9d..ba24c22b0 100644 --- a/lib/rspec/mocks/argument_list_matcher.rb +++ b/lib/rspec/mocks/argument_list_matcher.rb @@ -44,6 +44,7 @@ class ArgumentListMatcher # @see #args_match? def initialize(*expected_args) @expected_args = expected_args + ensure_expected_args_valid! end # @api public @@ -57,11 +58,6 @@ def args_match?(*args) Support::FuzzyMatcher.values_match?(matchers_for(args), args) end - # Value that will match all argument lists. - # - # @private - MATCH_ALL = new(ArgumentMatchers::AnyArgsMatcher::INSTANCE) - private def matchers_for(actual_args) @@ -80,6 +76,22 @@ def replace_any_args_with_splat_of_anything(before_count, actual_args_count) any_args = 1.upto(any_args_count).map { ArgumentMatchers::AnyArgMatcher::INSTANCE } expected_args.first(before_count) + any_args + expected_args.last(after_count) end + + def ensure_expected_args_valid! + if expected_args.count(ArgumentMatchers::AnyArgsMatcher::INSTANCE) > 1 + raise ArgumentError, "`any_args` can only be passed to " \ + "`with` once but you have passed it multiple times." + elsif expected_args.count > 1 && expected_args.include?(ArgumentMatchers::NoArgsMatcher::INSTANCE) + raise ArgumentError, "`no_args` can only be passed as a " \ + "singleton argument to `with` (i.e. `with(no_args)`), " \ + "but you have passed additional arguments." + end + end + + # Value that will match all argument lists. + # + # @private + MATCH_ALL = new(ArgumentMatchers::AnyArgsMatcher::INSTANCE) end end end diff --git a/spec/rspec/mocks/argument_matchers_spec.rb b/spec/rspec/mocks/argument_matchers_spec.rb index 6ecac474a..4e1e41461 100644 --- a/spec/rspec/mocks/argument_matchers_spec.rb +++ b/spec/rspec/mocks/argument_matchers_spec.rb @@ -216,6 +216,14 @@ module Mocks expect { a_double.random_call }.to fail_matching "expected: (1, *(any args), /foo/)" end end + + context "when passed twice" do + it 'immediately signals that this is invalid', :reset => true do + expect { + expect(a_double).to receive(:random_call).with(any_args, 1, any_args) + }.to raise_error(ArgumentError, /any_args/) + end + end end describe "no_args" do @@ -228,6 +236,14 @@ module Mocks expect(a_double).to receive(:msg).with(no_args) expect { a_double.msg(37) }.to fail_matching "expected: (no args)" end + + context "when passed with other arguments" do + it 'immediately signals that this is invalid', :reset => true do + expect { + expect(a_double).to receive(:random_call).with(no_args, 3) + }.to raise_error(ArgumentError, /no_args/) + end + end end describe "hash_including" do From a6eff993926e3a58e32574e66b0e65606e8ccd88 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Mon, 29 Sep 2014 07:55:20 -0700 Subject: [PATCH 4/6] Fix `with` verified double verification to work with new `any_args` semantics. --- lib/rspec/mocks/argument_list_matcher.rb | 11 ++-- .../mocks/verifying_message_expecation.rb | 26 +++------ .../expected_arg_verification_spec.rb | 55 +++++++++++++++++-- 3 files changed, 67 insertions(+), 25 deletions(-) diff --git a/lib/rspec/mocks/argument_list_matcher.rb b/lib/rspec/mocks/argument_list_matcher.rb index ba24c22b0..453bf9f11 100644 --- a/lib/rspec/mocks/argument_list_matcher.rb +++ b/lib/rspec/mocks/argument_list_matcher.rb @@ -55,12 +55,13 @@ def initialize(*expected_args) # # @see #initialize def args_match?(*args) - Support::FuzzyMatcher.values_match?(matchers_for(args), args) + Support::FuzzyMatcher.values_match?(resolve_expected_args_based_on(args), args) end - private - - def matchers_for(actual_args) + # @private + # Resolves abstract arg placeholders like `no_args` and `any_args` into + # a more concrete arg list based on the provided `actual_args`. + def resolve_expected_args_based_on(actual_args) return [] if [ArgumentMatchers::NoArgsMatcher::INSTANCE] == expected_args any_args_index = expected_args.index(ArgumentMatchers::AnyArgsMatcher::INSTANCE) @@ -69,6 +70,8 @@ def matchers_for(actual_args) replace_any_args_with_splat_of_anything(any_args_index, actual_args.count) end + private + def replace_any_args_with_splat_of_anything(before_count, actual_args_count) any_args_count = actual_args_count - expected_args.count + 1 after_count = expected_args.count - before_count - 1 diff --git a/lib/rspec/mocks/verifying_message_expecation.rb b/lib/rspec/mocks/verifying_message_expecation.rb index 6acdf8090..c81ef322b 100644 --- a/lib/rspec/mocks/verifying_message_expecation.rb +++ b/lib/rspec/mocks/verifying_message_expecation.rb @@ -23,31 +23,23 @@ def initialize(*args) # @private def with(*args, &block) - unless ArgumentMatchers::AnyArgsMatcher === args.first - expected_args = if ArgumentMatchers::NoArgsMatcher === args.first - [] - elsif args.length > 0 - args - else - # No arguments given, this will raise. - super - end - - validate_expected_arguments!(expected_args) + super(*args, &block).tap do + validate_expected_arguments! do |signature| + example_call_site_args = [:an_arg] * signature.min_non_kw_args + example_call_site_args << :kw_args_hash if signature.required_kw_args.any? + @argument_list_matcher.resolve_expected_args_based_on(example_call_site_args) + end end - super end private - def validate_expected_arguments!(actual_args) + def validate_expected_arguments! return if method_reference.nil? method_reference.with_signature do |signature| - verifier = Support::LooseSignatureVerifier.new( - signature, - actual_args - ) + args = yield signature + verifier = Support::LooseSignatureVerifier.new(signature, args) unless verifier.valid? # Fail fast is required, otherwise the message expecation will fail diff --git a/spec/rspec/mocks/verifying_doubles/expected_arg_verification_spec.rb b/spec/rspec/mocks/verifying_doubles/expected_arg_verification_spec.rb index 936ee7259..9baee520e 100644 --- a/spec/rspec/mocks/verifying_doubles/expected_arg_verification_spec.rb +++ b/spec/rspec/mocks/verifying_doubles/expected_arg_verification_spec.rb @@ -25,10 +25,32 @@ module Mocks after { reset dbl } context "when `any_args` is used" do - it "skips verification" do - expect { - expect(dbl).to receive(:instance_method_with_two_args).with(any_args) - }.not_to raise_error + context "as the only argument" do + it "is allowed regardless of how many args the method requires" do + expect { + expect(dbl).to receive(:instance_method_with_two_args).with(any_args) + }.not_to raise_error + end + end + + context "as the first argument, with too many additional args" do + it "is disallowed" do + expect { + expect(dbl).to receive(:instance_method_with_two_args).with(any_args, 1, 2, 3) + }.to fail_with("Wrong number of arguments. Expected 2, got 3.") + end + end + + context "as the first argument, with an allowed number of additional args" do + it "is allowed" do + expect { + expect(dbl).to receive(:instance_method_with_two_args).with(any_args, 1, 2) + }.not_to raise_error + + expect { + expect(dbl).to receive(:instance_method_with_two_args).with(any_args, 1) + }.not_to raise_error + end end end @@ -52,6 +74,31 @@ module Mocks expect(dbl).to receive(:instance_method_with_two_args).with(no_args) }.to fail_with("Wrong number of arguments. Expected 2, got 0.") end + + if RSpec::Support::RubyFeatures.required_kw_args_supported? + context "for a method with required keyword args" do + it 'covers the required args when `any_args` is last' do + expect { + expect(dbl).to receive(:kw_args_method).with(1, any_args) + }.not_to raise_error + end + + it 'does not cover the required args when there are args after `any_args`' do + expect { + # Use eval to avoid syntax error on 1.8 and 1.9 + eval("expect(dbl).to receive(:kw_args_method).with(any_args, optional_arg: 3)") + }.to fail_with("Missing required keyword arguments: required_arg") + end + end + end + end + + if RSpec::Support::RubyFeatures.required_kw_args_supported? + it 'does not cover required args when `any_args` is not used' do + expect { + expect(dbl).to receive(:kw_args_method).with(anything) + }.to fail_with("Missing required keyword arguments: required_arg") + end end context "when a list of args is provided" do From c3c94fa5b9606e528bd15772260312759f7e7f9b Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Tue, 30 Sep 2014 09:57:35 -0700 Subject: [PATCH 5/6] Update `any_args` docs to explain splat semantics. --- README.md | 5 +++-- .../setting_constraints/matching_arguments.feature | 1 + lib/rspec/mocks/argument_matchers.rb | 10 +++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8645df5c5..c87d48867 100644 --- a/README.md +++ b/README.md @@ -206,8 +206,9 @@ rspec-mocks also adds some keyword Symbols that you can use to specify certain kinds of arguments: ```ruby -expect(double).to receive(:msg).with(no_args()) -expect(double).to receive(:msg).with(any_args()) +expect(double).to receive(:msg).with(no_args) +expect(double).to receive(:msg).with(any_args) +expect(double).to receive(:msg).with(1, any_args) # any args acts like an arg splat and can go anywhere expect(double).to receive(:msg).with(1, kind_of(Numeric), "b") #2nd argument can be any kind of Numeric expect(double).to receive(:msg).with(1, boolean(), "b") #2nd argument can be true or false expect(double).to receive(:msg).with(1, /abc/, "b") #2nd argument can be any String matching the submitted Regexp diff --git a/features/setting_constraints/matching_arguments.feature b/features/setting_constraints/matching_arguments.feature index 269bc029e..638a2a368 100644 --- a/features/setting_constraints/matching_arguments.feature +++ b/features/setting_constraints/matching_arguments.feature @@ -9,6 +9,7 @@ Feature: Matching arguments | Literal arguments | `with(1, true)` | `foo(1, true)` | | Anything that supports case equality (`===`) | `with(/bar/)` | `foo("barn")` | | Any list of args | `with(any_args)` | `foo()`
`foo(1)`
`foo(:bar, 2)` | + | Any sublist of args (like an arg splat) | `with(1, any_args)` | `foo(1)`
`foo(1, :bar, :bazz)` | | An empty list of args | `with(no_args)` | `foo()` | | Anything for a given positional arg | `with(3, anything)` | `foo(3, nil)`
`foo(3, :bar)` | | Against an interface | `with(duck_type(:each))` | `foo([])` | diff --git a/lib/rspec/mocks/argument_matchers.rb b/lib/rspec/mocks/argument_matchers.rb index 7e08054f6..56052ce76 100644 --- a/lib/rspec/mocks/argument_matchers.rb +++ b/lib/rspec/mocks/argument_matchers.rb @@ -14,12 +14,16 @@ module Mocks # # @see ArgumentListMatcher module ArgumentMatchers - # Matches any args at all. Supports a more explicit variation of - # `expect(object).to receive(:message)` + # Acts like an arg splat, matching any number of args at any point in an arg list. # # @example # - # expect(object).to receive(:message).with(any_args) + # expect(object).to receive(:message).with(1, 2, any_args) + # + # # matches any of these: + # object.message(1, 2) + # object.message(1, 2, 3) + # object.message(1, 2, 3, 4) def any_args AnyArgsMatcher::INSTANCE end From 1cbb1c60fbe25016d2cf851814a20649ef8d7372 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Sun, 5 Oct 2014 15:42:32 -0700 Subject: [PATCH 6/6] =?UTF-8?q?Simplify=20wording=20based=20on=20@xaviersh?= =?UTF-8?q?ay=E2=80=99s=20suggestion.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/rspec/mocks/argument_matchers.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/rspec/mocks/argument_matchers.rb b/lib/rspec/mocks/argument_matchers.rb index 56052ce76..3e4a4ccea 100644 --- a/lib/rspec/mocks/argument_matchers.rb +++ b/lib/rspec/mocks/argument_matchers.rb @@ -131,12 +131,11 @@ def self.anythingize_lonely_keys(*args) end # Intended to be subclassed by stateless, immutable argument matchers. - # Provides a `::INSTANCE` constant for accessing a - # global singleton instance of the matcher. This saves on memory a bit - # (as their is no need to construct multiple instance since there is - # no internal instance state). It also facilities the special case logic - # we need for some of these matchers, by making it easy to do comparisons - # like: `[klass::INSTANCE] == args` rather than + # Provides a `::INSTANCE` constant for accessing a global + # singleton instance of the matcher. There is no need to construct + # multiple instance since there is no state. It also facilities the + # special case logic we need for some of these matchers, by making it + # easy to do comparisons like: `[klass::INSTANCE] == args` rather than # `args.count == 1 && klass === args.first`. # # @private