Skip to content

Commit

Permalink
add IncludesExactly matcher
Browse files Browse the repository at this point in the history
  • Loading branch information
mohammednasser-32 committed Oct 7, 2024
1 parent c4bd4af commit 911fa7f
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/mocha/parameter_matchers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
115 changes: 115 additions & 0 deletions lib/mocha/parameter_matchers/includes_exactly.rb
Original file line number Diff line number Diff line change
@@ -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
111 changes: 111 additions & 0 deletions test/unit/parameter_matchers/includes_exactly.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 911fa7f

Please sign in to comment.