From 911fa7f2b10b5eb4e8050bfbdf97885bff17ab71 Mon Sep 17 00:00:00 2001 From: mohammednasser-32 Date: Mon, 7 Oct 2024 21:30:58 +0300 Subject: [PATCH] add IncludesExactly matcher --- lib/mocha/parameter_matchers.rb | 1 + .../parameter_matchers/includes_exactly.rb | 115 ++++++++++++++++++ .../parameter_matchers/includes_exactly.rb | 111 +++++++++++++++++ 3 files changed, 227 insertions(+) create mode 100644 lib/mocha/parameter_matchers/includes_exactly.rb create mode 100644 test/unit/parameter_matchers/includes_exactly.rb diff --git a/lib/mocha/parameter_matchers.rb b/lib/mocha/parameter_matchers.rb index d18fcf9c2..43553d6e9 100644 --- a/lib/mocha/parameter_matchers.rb +++ b/lib/mocha/parameter_matchers.rb @@ -25,3 +25,4 @@ module ParameterMatchers; end require 'mocha/parameter_matchers/responds_with' require 'mocha/parameter_matchers/yaml_equivalent' require 'mocha/parameter_matchers/equivalent_uri' +require 'mocha/parameter_matchers/includes_exactly' diff --git a/lib/mocha/parameter_matchers/includes_exactly.rb b/lib/mocha/parameter_matchers/includes_exactly.rb new file mode 100644 index 000000000..8ad2b084f --- /dev/null +++ b/lib/mocha/parameter_matchers/includes_exactly.rb @@ -0,0 +1,115 @@ +require 'mocha/parameter_matchers/base' + +module Mocha + module ParameterMatchers + # Matches any object that responds with +true+ to +include?(item)+ + # for all items, taking into account that each object element should+ + # be matched with a different item + # + # @param [*Array] items expected items. + # @return [IncludesExactly] parameter matcher. + # + # @see Expectation#with + # + # @example Actual parameter includes exact items. + # object = mock() + # object.expects(:method_1).with(includes_exactly('foo', 'bar')) + # object.method_1(['bar', 'foo']) + # # no error raised + # + # @example Actual parameter does not include exact items. + # object.method_1(['foo', 'bar', 'bar']) + # # error raised, because ['foo', 'bar', 'bar'] has an extra 'bar'. + # + # @example Actual parameter does not include exact items. + # object.method_1(['foo', 'baz']) + # # error raised, because ['foo', 'baz'] does not include 'bar'. + # + # @example Items does not include all actual parameters. + # object.method_1(['foo', 'bar', 'baz]) + # # error raised, because ['foo', 'bar'] does not include 'baz'. + # + # @example Actual parameter includes item which matches nested matcher. + # object = mock() + # object.expects(:method_1).with(includes_exactly(has_key(:key), 'foo', 'bar')) + # object.method_1(['foo', 'bar', {key: 'baz'}]) + # # no error raised + # + # @example Actual parameter does not include item matching nested matcher. + # object.method_1(['foo', 'bar', {:other_key => 'baz'}]) + # # error raised, because no element matches `has_key(:key)` matcher + # + # @example Actual parameter is the exact item String. + # object = mock() + # object.expects(:method_1).with(includes_exactly('bar')) + # object.method_1('bar') + # # no error raised + # + # @example Actual parameter is a String including substring. + # object.method_1('foobar') + # # error raised, because 'foobar' is not equal 'bar' + # + # @example Actual parameter is a Hash including the exact keys. + # object = mock() + # object.expects(:method_1).with(includes_exactly(:bar)) + # object.method_1({bar: 2}) + # # no error raised + # + # @example Actual parameter is a Hash including an extra key. + # object = mock() + # object.expects(:method_1).with(includes_exactly(:bar)) + # object.method_1({foo: 1, bar: 2,}) + # # error raised, because items does not include :foo + # + # @example Actual parameter is a Hash without the given key. + # object.method_1({foo: 1}) + # # error raised, because hash does not include key 'bar' + # + # @example Actual parameter is a Hash with a key matching the given matcher. + # object = mock() + # object.expects(:method_1).with(includes_exactly(regexp_matches(/ar/))) + # object.method_1({'bar' => 2}) + # # no error raised + # + # @example Actual parameter is a Hash no key matching the given matcher. + # object.method_1({'baz' => 3}) + # # error raised, because hash does not include a key matching /ar/ + def includes_exactly(*items) + IncludesExactly.new(*items) + end + + # Parameter matcher which matches when actual parameter includes expected values. + class IncludesExactly < Base + # @private + def initialize(*items) + @items = items + end + + # @private + # rubocop:disable Metrics/PerceivedComplexity + def matches?(available_parameters) + parameters = available_parameters.shift + return false unless parameters.respond_to?(:include?) + return parameters == @items.first if parameters.is_a?(String) && @items.size == 1 + + parameters = parameters.keys if parameters.is_a?(Hash) + + @items.each do |item| + matched_index = parameters.each_index.find { |i| item.to_matcher.matches?([parameters[i]]) } + return false unless matched_index + + parameters.delete_at(matched_index) + end + + parameters.empty? + end + # rubocop:enable Metrics/PerceivedComplexity + + # @private + def mocha_inspect + item_descriptions = @items.map(&:mocha_inspect) + "includes_exactly(#{item_descriptions.join(', ')})" + end + end + end +end diff --git a/test/unit/parameter_matchers/includes_exactly.rb b/test/unit/parameter_matchers/includes_exactly.rb new file mode 100644 index 000000000..cc4d69792 --- /dev/null +++ b/test/unit/parameter_matchers/includes_exactly.rb @@ -0,0 +1,111 @@ +require File.expand_path('../../../test_helper', __FILE__) + +require 'mocha/parameter_matchers/includes_exactly' +require 'mocha/parameter_matchers/instance_methods' +require 'mocha/parameter_matchers/has_key' +require 'mocha/parameter_matchers/regexp_matches' +require 'mocha/inspect' + +class IncludesExactlyTest < Mocha::TestCase + include Mocha::ParameterMatchers + + def test_should_match_object_including_array_with_exact_values + matcher = includes_exactly(:x, :y, :z) + assert matcher.matches?([[:y, :z, :x]]) + end + + def test_should_not_match_object_that_does_not_include_value + matcher = includes_exactly(:not_included) + assert !matcher.matches?([[:x, :y, :z]]) + end + + def test_should_not_match_object_that_does_not_include_any_one_value + matcher = includes_exactly(:x, :y, :z, :not_included) + assert !matcher.matches?([[:x, :y, :z]]) + end + + def test_should_not_match_object_that_does_not_include_all_values + matcher = includes_exactly(:x, :y) + assert !matcher.matches?([[:x, :y, :z]]) + end + + def test_should_not_match_if_number_of_occurances_is_not_identical + matcher = includes_exactly(:x, :y, :y) + assert !matcher.matches?([[:x, :x, :y]]) + end + + def test_should_describe_matcher_with_one_item + matcher = includes_exactly(:x) + assert_equal 'includes_exactly(:x)', matcher.mocha_inspect + end + + def test_should_describe_matcher_with_multiple_items + matcher = includes_exactly(:x, :y, :z) + assert_equal 'includes_exactly(:x, :y, :z)', matcher.mocha_inspect + end + + def test_should_not_raise_error_on_emtpy_arguments + matcher = includes_exactly(:x) + assert_nothing_raised { matcher.matches?([]) } + end + + def test_should_not_match_on_empty_arguments + matcher = includes_exactly(:x) + assert !matcher.matches?([]) + end + + def test_should_not_match_on_empty_array_arguments + matcher = includes_exactly(:x) + assert !matcher.matches?([[]]) + end + + def test_should_not_raise_error_on_argument_that_does_not_respond_to_include + matcher = includes_exactly(:x) + assert_nothing_raised { matcher.matches?([:x]) } + end + + def test_should_not_match_on_argument_that_does_not_respond_to_include + matcher = includes_exactly(:x) + assert !matcher.matches?([:x]) + end + + def test_should_match_object_with_nested_matchers + matcher = includes_exactly(has_key(:key1), :x) + assert matcher.matches?([[:x, { key1: 'value' }]]) + end + + def test_should_not_match_object_with_an_unmatched_nested_matchers + matcher = includes_exactly(has_key(:key1), :x) + assert !matcher.matches?([[:x, { no_match: 'value' }]]) + end + + def test_should_not_match_string_argument_containing_substring + matcher = includes_exactly('bar') + assert !matcher.matches?(['foobarbaz']) + end + + def test_should_match_exact_string_argument + matcher = includes_exactly('bar') + assert matcher.matches?(['bar']) + end + + def test_should_match_hash_argument_containing_exact_keys + matcher = includes_exactly(:key1, :key2) + assert matcher.matches?([{ key2: 1, key1: 2 }]) + end + + def test_should_not_match_hash_argument_not_matching_all_keys + matcher = includes_exactly(:key) + assert !matcher.matches?([{ thing: 1, key: 2 }]) + end + + def test_should_match_hash_when_nested_matcher_matches_key + matcher = includes_exactly(regexp_matches(/ar/), 'foo') + assert matcher.matches?([{ 'foo' => 1, 'bar' => 2 }]) + end + + def test_should_not_match_hash_when_nested_matcher_doesn_not_match_key + matcher = includes_exactly(regexp_matches(/az/), 'foo') + assert !matcher.matches?([{ 'foo' => 1, 'bar' => 2 }]) + end +end