From fa8a55eaa317ce9b38d92df905d6539b553e0eed Mon Sep 17 00:00:00 2001 From: Collin Styles Date: Sat, 12 Jun 2021 12:29:21 -0700 Subject: [PATCH 1/3] Add `HasKeys` parameter matcher Much like how `HasEntry` has a related `HasEntries` matcher, this commit adds a `HasKeys` matcher to accompany `HasKey`. With this new matcher, instead of the following: ```ruby object.expects(:method).with(all_of(has_key(:key_1), has_key(:key_2), has_key(:key_3))) ``` We can just do this: ```ruby object.expects(:method).with(has_keys(:key_1, :key_2, :key_3)) ``` --- lib/mocha/parameter_matchers.rb | 1 + lib/mocha/parameter_matchers/has_keys.rb | 51 ++++++++++++++++ test/unit/parameter_matchers/has_keys_test.rb | 59 +++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 lib/mocha/parameter_matchers/has_keys.rb create mode 100644 test/unit/parameter_matchers/has_keys_test.rb diff --git a/lib/mocha/parameter_matchers.rb b/lib/mocha/parameter_matchers.rb index 7a64ad6dd..d18fcf9c2 100644 --- a/lib/mocha/parameter_matchers.rb +++ b/lib/mocha/parameter_matchers.rb @@ -13,6 +13,7 @@ module ParameterMatchers; end require 'mocha/parameter_matchers/has_entry' require 'mocha/parameter_matchers/has_entries' require 'mocha/parameter_matchers/has_key' +require 'mocha/parameter_matchers/has_keys' require 'mocha/parameter_matchers/has_value' require 'mocha/parameter_matchers/includes' require 'mocha/parameter_matchers/instance_of' diff --git a/lib/mocha/parameter_matchers/has_keys.rb b/lib/mocha/parameter_matchers/has_keys.rb new file mode 100644 index 000000000..fbfb91270 --- /dev/null +++ b/lib/mocha/parameter_matchers/has_keys.rb @@ -0,0 +1,51 @@ +require 'mocha/parameter_matchers/base' + +module Mocha + module ParameterMatchers + # Matches +Hash+ containing +keys+. + # + # @param [Object] keys expected keys. + # @return [HasKeys] parameter matcher. + # + # @see Expectation#with + # + # @example Actual parameter contains entry with expected keys. + # object = mock() + # object.expects(:method_1).with(has_keys(:key_1, :key_2)) + # object.method_1(:key_1 => 1, :key_2 => 2, :key_3 => 3) + # # no error raised + # + # @example Actual parameter does not contain entry with all expected keys. + # object = mock() + # object.expects(:method_1).with(has_keys(:key_1, :key_2)) + # object.method_1(:key_2 => 2) + # # error raised, because method_1 was not called with Hash containing key: :key_1 + # + def has_keys(*keys) # rubocop:disable Naming/PredicateName + HasKeys.new(*keys) + end + + # Parameter matcher which matches when actual parameter contains +Hash+ entry with expected keys. + class HasKeys < Base + # @private + def initialize(*keys) + @keys = keys + end + + # @private + def matches?(available_parameters) + parameter = available_parameters.shift + return false unless parameter.respond_to?(:keys) + + @keys.map(&:to_matcher).all? do |matcher| + parameter.keys.any? { |key| matcher.matches?([key]) } + end + end + + # @private + def mocha_inspect + "has_keys(#{@keys.mocha_inspect})" + end + end + end +end diff --git a/test/unit/parameter_matchers/has_keys_test.rb b/test/unit/parameter_matchers/has_keys_test.rb new file mode 100644 index 000000000..3f94a6cb9 --- /dev/null +++ b/test/unit/parameter_matchers/has_keys_test.rb @@ -0,0 +1,59 @@ +require File.expand_path('../../../test_helper', __FILE__) + +require 'mocha/parameter_matchers/has_keys' +require 'mocha/parameter_matchers/instance_methods' +require 'mocha/inspect' + +class HasKeysTest < Mocha::TestCase + include Mocha::ParameterMatchers + + def test_should_match_hash_including_specified_keys + matcher = has_keys(:key_1, :key_2) + assert matcher.matches?([{ :key_1 => 1, :key_2 => 2, :key_3 => 3 }]) + end + + def test_should_not_match_hash_not_including_specified_keys + matcher = has_keys(:key_1, :key_2) + assert !matcher.matches?([{ :key_3 => 3 }]) + end + + def test_should_not_match_hash_not_including_all_keys + matcher = has_keys(:key_1, :key_2) + assert !matcher.matches?([{ :key_1 => 1, :key_3 => 3 }]) + end + + def test_should_describe_matcher + matcher = has_keys(:key_1, :key_2) + assert_equal 'has_keys([:key_1, :key_2])', matcher.mocha_inspect + end + + def test_should_match_hash_including_specified_key_with_nested_key_matcher + matcher = has_keys(equals(:key_1), equals(:key_2)) + assert matcher.matches?([{ :key_1 => 1, :key_2 => 2 }]) + end + + def test_should_not_match_hash_not_including_specified_keys_with_nested_key_matchers + matcher = has_keys(equals(:key_1), equals(:key2)) + assert !matcher.matches?([{ :key_2 => 2 }]) + end + + def test_should_not_raise_error_on_empty_arguments + matcher = has_keys(:key_1, :key_2) + assert_nothing_raised { matcher.matches?([]) } + end + + def test_should_not_match_on_empty_arguments + matcher = has_keys(:key_1, :key_2) + assert !matcher.matches?([]) + end + + def test_should_not_raise_error_on_argument_that_does_not_respond_to_keys + matcher = has_keys(:key_1, :key_2) + assert_nothing_raised { matcher.matches?([:key_1]) } + end + + def test_should_not_match_on_argument_that_does_not_respond_to_keys + matcher = has_keys(:key_1, :key_2) + assert !matcher.matches?([:key_1]) + end +end From addb9f3d64034c5120708826886087ce2c40a42a Mon Sep 17 00:00:00 2001 From: Collin Styles Date: Sat, 12 Jun 2021 12:57:51 -0700 Subject: [PATCH 2/3] Trim brackets from `HasKeys#mocha_inspect` output --- lib/mocha/parameter_matchers/has_keys.rb | 2 +- test/unit/parameter_matchers/has_keys_test.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mocha/parameter_matchers/has_keys.rb b/lib/mocha/parameter_matchers/has_keys.rb index fbfb91270..0938f7740 100644 --- a/lib/mocha/parameter_matchers/has_keys.rb +++ b/lib/mocha/parameter_matchers/has_keys.rb @@ -44,7 +44,7 @@ def matches?(available_parameters) # @private def mocha_inspect - "has_keys(#{@keys.mocha_inspect})" + "has_keys(#{@keys.mocha_inspect[1...-1]})" end end end diff --git a/test/unit/parameter_matchers/has_keys_test.rb b/test/unit/parameter_matchers/has_keys_test.rb index 3f94a6cb9..ee8cd1d60 100644 --- a/test/unit/parameter_matchers/has_keys_test.rb +++ b/test/unit/parameter_matchers/has_keys_test.rb @@ -24,7 +24,7 @@ def test_should_not_match_hash_not_including_all_keys def test_should_describe_matcher matcher = has_keys(:key_1, :key_2) - assert_equal 'has_keys([:key_1, :key_2])', matcher.mocha_inspect + assert_equal 'has_keys(:key_1, :key_2)', matcher.mocha_inspect end def test_should_match_hash_including_specified_key_with_nested_key_matcher From db954937c54961b0efe53dd8f1ba6582f572862d Mon Sep 17 00:00:00 2001 From: Collin Styles Date: Sat, 12 Jun 2021 13:04:35 -0700 Subject: [PATCH 3/3] Raise `ArgumentError` if no keys are supplied to `has_keys` --- lib/mocha/parameter_matchers/has_keys.rb | 2 ++ test/unit/parameter_matchers/has_keys_test.rb | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/lib/mocha/parameter_matchers/has_keys.rb b/lib/mocha/parameter_matchers/has_keys.rb index 0938f7740..a0bb2b71e 100644 --- a/lib/mocha/parameter_matchers/has_keys.rb +++ b/lib/mocha/parameter_matchers/has_keys.rb @@ -29,6 +29,8 @@ def has_keys(*keys) # rubocop:disable Naming/PredicateName class HasKeys < Base # @private def initialize(*keys) + raise ArgumentError, 'Argument has no keys.' if keys.empty? + @keys = keys end diff --git a/test/unit/parameter_matchers/has_keys_test.rb b/test/unit/parameter_matchers/has_keys_test.rb index ee8cd1d60..00940f7c0 100644 --- a/test/unit/parameter_matchers/has_keys_test.rb +++ b/test/unit/parameter_matchers/has_keys_test.rb @@ -56,4 +56,9 @@ def test_should_not_match_on_argument_that_does_not_respond_to_keys matcher = has_keys(:key_1, :key_2) assert !matcher.matches?([:key_1]) end + + def test_should_raise_argument_error_if_no_keys_are_supplied + e = assert_raises(ArgumentError) { has_keys } + assert_equal 'Argument has no keys.', e.message + end end