diff --git a/lib/mocha/parameter_matchers/has_entries.rb b/lib/mocha/parameter_matchers/has_entries.rb index 1b6d18d81..2b90f34b8 100644 --- a/lib/mocha/parameter_matchers/has_entries.rb +++ b/lib/mocha/parameter_matchers/has_entries.rb @@ -30,20 +30,23 @@ def has_entries(entries) # rubocop:disable Naming/PredicateName # Parameter matcher which matches when actual parameter contains all expected +Hash+ entries. class HasEntries < Base # @private - def initialize(entries) + def initialize(entries, exact: false) @entries = entries + @exact = exact end # @private def matches?(available_parameters) parameter = available_parameters.shift + return false if @exact && @entries.length != parameter.length + has_entry_matchers = @entries.map { |key, value| HasEntry.new(key, value) } AllOf.new(*has_entry_matchers).matches?([parameter]) end # @private def mocha_inspect - "has_entries(#{@entries.mocha_inspect})" + @exact ? @entries.mocha_inspect : "has_entries(#{@entries.mocha_inspect})" end end end diff --git a/lib/mocha/parameter_matchers/positional_or_keyword_hash.rb b/lib/mocha/parameter_matchers/positional_or_keyword_hash.rb index d9b77c428..341d314b8 100644 --- a/lib/mocha/parameter_matchers/positional_or_keyword_hash.rb +++ b/lib/mocha/parameter_matchers/positional_or_keyword_hash.rb @@ -1,6 +1,7 @@ require 'mocha/configuration' require 'mocha/deprecation' require 'mocha/parameter_matchers/base' +require 'mocha/parameter_matchers/has_entries' module Mocha module ParameterMatchers @@ -14,7 +15,7 @@ def initialize(value, expectation) def matches?(available_parameters) parameter, is_last_parameter = extract_parameter(available_parameters) - return false unless HasEntries.new(@value).matches?([parameter]) + return false unless HasEntries.new(@value, exact: true).matches?([parameter]) if is_last_parameter && !same_type_of_hash?(parameter, @value) return false if Mocha.configuration.strict_keyword_argument_matching? diff --git a/test/acceptance/parameter_matcher_test.rb b/test/acceptance/parameter_matcher_test.rb index d982539fa..49cb58226 100644 --- a/test/acceptance/parameter_matcher_test.rb +++ b/test/acceptance/parameter_matcher_test.rb @@ -11,6 +11,24 @@ def teardown teardown_acceptance_test end + def test_should_match_hash_parameter_which_is_exactly_the_same + test_result = run_as_test do + mock = mock() + mock.expects(:method).with(key_1: 'value_1') + mock.method(key_1: 'value_1') + end + assert_passed(test_result) + end + + def test_should_not_match_hash_parameter_which_is_not_exactly_the_same + test_result = run_as_test do + mock = mock() + mock.expects(:method).with(key_1: 'value_1') + mock.method(key_1: 'value_1', key_2: 'value_2') + end + assert_failed(test_result) + end + def test_should_match_hash_parameter_with_specified_key test_result = run_as_test do mock = mock() @@ -137,6 +155,33 @@ def test_should_not_match_hash_parameter_with_specified_entries_using_nested_mat assert_failed(test_result) end + def test_should_match_hash_parameter_that_is_exactly_a_key_that_is_a_string_with_a_value_that_is_an_integer + test_result = run_as_test do + mock = mock() + mock.expects(:method).with(is_a(String) => is_a(Integer)) + mock.method('key_1' => 123) + end + assert_passed(test_result) + end + + def test_should_not_match_hash_parameter_that_is_exactly_a_key_that_is_a_string_with_a_value_that_is_an_integer_because_value_not_integer + test_result = run_as_test do + mock = mock() + mock.expects(:method).with(is_a(String) => is_a(Integer)) + mock.method('key_1' => '123') + end + assert_failed(test_result) + end + + def test_should_not_match_hash_parameter_that_is_exactly_a_key_that_is_a_string_with_a_value_that_is_an_integer_because_of_extra_entry + test_result = run_as_test do + mock = mock() + mock.expects(:method).with(is_a(String) => is_a(Integer)) + mock.method('key_1' => 123, 'key_2' => 'doesntmatter') + end + assert_failed(test_result) + end + def test_should_match_parameter_that_matches_any_value test_result = run_as_test do mock = mock() diff --git a/test/unit/parameter_matchers/positional_or_keyword_hash_test.rb b/test/unit/parameter_matchers/positional_or_keyword_hash_test.rb index 005246a3d..591a01b9e 100644 --- a/test/unit/parameter_matchers/positional_or_keyword_hash_test.rb +++ b/test/unit/parameter_matchers/positional_or_keyword_hash_test.rb @@ -11,38 +11,58 @@ class PositionalOrKeywordHashTest < Mocha::TestCase include Mocha::ParameterMatchers def test_should_describe_matcher - matcher = { key_1: 1, key_2: 2 }.to_matcher + hash = { key_1: 1, key_2: 2 } + matcher = build_matcher(hash) assert_equal '{:key_1 => 1, :key_2 => 2}', matcher.mocha_inspect end def test_should_match_non_last_hash_arg_with_hash_arg - matcher = { key_1: 1, key_2: 2 }.to_matcher + hash = { key_1: 1, key_2: 2 } + matcher = build_matcher(hash) assert matcher.matches?([{ key_1: 1, key_2: 2 }, %w[a b]]) end def test_should_not_match_non_hash_arg_with_hash_arg - matcher = { key_1: 1, key_2: 2 }.to_matcher + hash = { key_1: 1, key_2: 2 } + matcher = build_matcher(hash) assert !matcher.matches?([%w[a b]]) end def test_should_match_hash_arg_with_hash_arg - matcher = { key_1: 1, key_2: 2 }.to_matcher + hash = { key_1: 1, key_2: 2 } + matcher = build_matcher(hash) assert matcher.matches?([{ key_1: 1, key_2: 2 }]) end + def test_should_not_match_hash_arg_with_different_hash_arg + hash = { key_1: 1 } + matcher = build_matcher(hash) + assert !matcher.matches?([{ key_1: 1, key_2: 2 }]) + end + def test_should_match_keyword_args_with_keyword_args - matcher = Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 }).to_matcher # rubocop:disable Style/BracesAroundHashParameters + matcher = build_matcher(Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })) # rubocop:disable Style/BracesAroundHashParameters assert matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters end + def test_should_not_match_keyword_args_with_different_keyword_args + matcher = build_matcher(Hash.ruby2_keywords_hash({ key_1: 1 })) # rubocop:disable Style/BracesAroundHashParameters + assert !matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters + end + def test_should_match_keyword_args_with_matchers_using_keyword_args - matcher = Hash.ruby2_keywords_hash({ key_1: is_a(String), key_2: is_a(Integer) }).to_matcher(top_level: true) # rubocop:disable Style/BracesAroundHashParameters + matcher = build_matcher(Hash.ruby2_keywords_hash({ key_1: is_a(String), key_2: is_a(Integer) })) # rubocop:disable Style/BracesAroundHashParameters assert matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 'foo', key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters end + def test_should_not_match_keyword_args_with_matchers_using_keyword_args_when_not_all_entries_are_matched + matcher = build_matcher(Hash.ruby2_keywords_hash({ key_1: is_a(String) })) # rubocop:disable Style/BracesAroundHashParameters + assert !matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 'foo', key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters + end + def test_should_match_hash_arg_with_keyword_args_but_display_deprecation_warning_if_appropriate expectation = Mocha::Expectation.new(self, :foo); execution_point = ExecutionPoint.current - matcher = Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 }).to_matcher(expectation: expectation, top_level: true) # rubocop:disable Style/BracesAroundHashParameters + matcher = build_matcher(Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 }), expectation) # rubocop:disable Style/BracesAroundHashParameters DeprecationDisabler.disable_deprecations do assert matcher.matches?([{ key_1: 1, key_2: 2 }]) end @@ -58,7 +78,7 @@ def test_should_match_hash_arg_with_keyword_args_but_display_deprecation_warning def test_should_match_keyword_args_with_hash_arg_but_display_deprecation_warning_if_appropriate expectation = Mocha::Expectation.new(self, :foo); execution_point = ExecutionPoint.current - matcher = { key_1: 1, key_2: 2 }.to_matcher(expectation: expectation, top_level: true) + matcher = build_matcher({ key_1: 1, key_2: 2 }, expectation) DeprecationDisabler.disable_deprecations do assert matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters end @@ -74,42 +94,46 @@ def test_should_match_keyword_args_with_hash_arg_but_display_deprecation_warning if Mocha::RUBY_V27_PLUS def test_should_match_non_last_hash_arg_with_hash_arg_when_strict_keyword_args_is_enabled - matcher = { key_1: 1, key_2: 2 }.to_matcher + hash = { key_1: 1, key_2: 2 } + matcher = build_matcher(hash) Mocha::Configuration.override(strict_keyword_argument_matching: true) do assert matcher.matches?([{ key_1: 1, key_2: 2 }, %w[a b]]) end end def test_should_not_match_non_hash_arg_with_hash_arg_when_strict_keyword_args_is_enabled - matcher = { key_1: 1, key_2: 2 }.to_matcher + hash = { key_1: 1, key_2: 2 } + matcher = build_matcher(hash) Mocha::Configuration.override(strict_keyword_argument_matching: true) do assert !matcher.matches?([%w[a b]]) end end def test_should_match_hash_arg_with_hash_arg_when_strict_keyword_args_is_enabled - matcher = { key_1: 1, key_2: 2 }.to_matcher + hash = { key_1: 1, key_2: 2 } + matcher = build_matcher(hash) Mocha::Configuration.override(strict_keyword_argument_matching: true) do assert matcher.matches?([{ key_1: 1, key_2: 2 }]) end end def test_should_match_keyword_args_with_keyword_args_when_strict_keyword_args_is_enabled - matcher = Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 }).to_matcher # rubocop:disable Style/BracesAroundHashParameters + matcher = build_matcher(Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })) # rubocop:disable Style/BracesAroundHashParameters Mocha::Configuration.override(strict_keyword_argument_matching: true) do assert matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters end end def test_should_not_match_hash_arg_with_keyword_args_when_strict_keyword_args_is_enabled - matcher = Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 }).to_matcher(top_level: true) # rubocop:disable Style/BracesAroundHashParameters + matcher = build_matcher(Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })) # rubocop:disable Style/BracesAroundHashParameters Mocha::Configuration.override(strict_keyword_argument_matching: true) do assert !matcher.matches?([{ key_1: 1, key_2: 2 }]) end end def test_should_not_match_keyword_args_with_hash_arg_when_strict_keyword_args_is_enabled - matcher = { key_1: 1, key_2: 2 }.to_matcher(top_level: true) + hash = { key_1: 1, key_2: 2 } + matcher = build_matcher(hash) Mocha::Configuration.override(strict_keyword_argument_matching: true) do assert !matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters end @@ -117,7 +141,7 @@ def test_should_not_match_keyword_args_with_hash_arg_when_strict_keyword_args_is def test_should_display_deprecation_warning_even_if_parent_expectation_is_nil expectation = nil - matcher = { key_1: 1, key_2: 2 }.to_matcher(expectation: expectation, top_level: true) + matcher = build_matcher({ key_1: 1, key_2: 2 }, expectation) DeprecationDisabler.disable_deprecations do matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters end @@ -127,4 +151,10 @@ def test_should_display_deprecation_warning_even_if_parent_expectation_is_nil assert_includes message, 'but received keyword arguments (key_1: 1, key_2: 2)' end end + + private + + def build_matcher(hash, expectation = nil) + Mocha::ParameterMatchers::PositionalOrKeywordHash.new(hash, expectation) + end end