Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IncludesExactly matcher #674

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'
113 changes: 113 additions & 0 deletions lib/mocha/parameter_matchers/includes_exactly.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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
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

# @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