Skip to content

Commit

Permalink
Merge pull request #698 from freerange/expectation-with-improvements
Browse files Browse the repository at this point in the history
`Expectation#with` improvements
  • Loading branch information
floehopper authored Dec 8, 2024
2 parents dc42922 + b30e443 commit 4d4a027
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 7 deletions.
29 changes: 25 additions & 4 deletions lib/mocha/expectation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ def at_most_once
#
# May be used with Ruby literals or variables for exact matching or with parameter matchers for less-specific matching, e.g. {ParameterMatchers#includes}, {ParameterMatchers#has_key}, etc. See {ParameterMatchers} for a list of all available parameter matchers.
#
# Alternatively a block argument can be passed to {#with} to implement custom parameter matching. The block receives the +*actual_parameters+ as its arguments and should return +true+ if they are acceptable or +false+ otherwise. See the example below where a method is expected to be called with a value divisible by 4.
# The block argument takes precedence over +expected_parameters_or_matchers+. The block may be called multiple times per invocation of the expected method and so it should be idempotent.
#
# Note that if {#with} is called multiple times on the same expectation, the last call takes precedence; other calls are ignored.
#
# Positional arguments were separated from keyword arguments in Ruby v3 (see {https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0 this article}). In relation to this a new configuration option ({Configuration#strict_keyword_argument_matching=}) is available in Ruby >= 2.7.
#
# When {Configuration#strict_keyword_argument_matching=} is set to +false+ (which is currently the default), a positional +Hash+ and a set of keyword arguments passed to {#with} are treated the same for the purposes of parameter matching. However, a deprecation warning will be displayed if a positional +Hash+ matches a set of keyword arguments or vice versa. This is because {Configuration#strict_keyword_argument_matching=} will default to +true+ in the future.
Expand All @@ -202,10 +207,10 @@ def at_most_once
# @see ParameterMatchers
# @see Configuration#strict_keyword_argument_matching=
#
# @param [*Array<Object,ParameterMatchers::Base>] expected_parameters_or_matchers expected parameter values or parameter matchers.
# @param [Array<Object,ParameterMatchers::Base>] expected_parameters_or_matchers expected parameter values or parameter matchers.
# @yield optional block specifying custom matching.
# @yieldparam [*Array<Object>] actual_parameters parameters with which expected method was invoked.
# @yieldreturn [Boolean] +true+ if +actual_parameters+ are acceptable.
# @yieldparam [Array<Object>] actual_parameters parameters with which expected method was invoked.
# @yieldreturn [Boolean] +true+ if +actual_parameters+ are acceptable; +false+ otherwise.
# @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained.
#
# @example Expected method must be called with exact parameter values.
Expand Down Expand Up @@ -256,7 +261,7 @@ def at_most_once
# example.foo('a', { bar: 'b' })
# # This now fails as expected
#
# @example Expected method must be called with a value divisible by 4.
# @example Using a block argument to expect the method to be called with a value divisible by 4.
# object = mock()
# object.expects(:expected_method).with() { |value| value % 4 == 0 }
# object.expected_method(16)
Expand All @@ -266,6 +271,22 @@ def at_most_once
# object.expects(:expected_method).with() { |value| value % 4 == 0 }
# object.expected_method(17)
# # => verify fails
#
# @example Extracting a custom matcher into an instance method on the test class.
# class MyTest < Minitest::Test
# def test_expected_method_is_called_with_a_value_divisible_by_4
# object = mock()
# object.expects(:expected_method).with(&method(:divisible_by_4))
# object.expected_method(16)
# # => verify succeeds
# end
#
# private
#
# def divisible_by_4(value)
# value % 4 == 0
# end
# end
def with(*expected_parameters_or_matchers, &matching_block)
@parameters_matcher = ParametersMatcher.new(expected_parameters_or_matchers, self, &matching_block)
self
Expand Down
10 changes: 7 additions & 3 deletions lib/mocha/parameters_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ def parameters_match?(actual_parameters)
end

def mocha_inspect
signature = matchers.mocha_inspect
signature = signature.gsub(/^\[|\]$/, '')
"(#{signature})"
if @matching_block
'(arguments_accepted_by_custom_matching_block)'
else
signature = matchers.mocha_inspect
signature = signature.gsub(/^\[|\]$/, '')
"(#{signature})"
end
end

def matchers
Expand Down
5 changes: 5 additions & 0 deletions test/unit/parameters_matcher_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,9 @@ def test_should_indicate_that_matcher_will_match_any_actual_parameters
parameters_matcher = ParametersMatcher.new
assert_equal '(any_parameters)', parameters_matcher.mocha_inspect
end

def test_should_indicate_that_matcher_logic_is_defined_by_custom_block
parameters_matcher = ParametersMatcher.new { true }
assert_equal '(arguments_accepted_by_custom_matching_block)', parameters_matcher.mocha_inspect
end
end

0 comments on commit 4d4a027

Please sign in to comment.