diff --git a/lib/mocha/expectation.rb b/lib/mocha/expectation.rb index f55918bd..f7c06b51 100644 --- a/lib/mocha/expectation.rb +++ b/lib/mocha/expectation.rb @@ -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. @@ -202,10 +207,10 @@ def at_most_once # @see ParameterMatchers # @see Configuration#strict_keyword_argument_matching= # - # @param [*Array] expected_parameters_or_matchers expected parameter values or parameter matchers. + # @param [Array] expected_parameters_or_matchers expected parameter values or parameter matchers. # @yield optional block specifying custom matching. - # @yieldparam [*Array] actual_parameters parameters with which expected method was invoked. - # @yieldreturn [Boolean] +true+ if +actual_parameters+ are acceptable. + # @yieldparam [Array] 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. @@ -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) @@ -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 diff --git a/lib/mocha/parameters_matcher.rb b/lib/mocha/parameters_matcher.rb index 72645fe6..da1abb54 100644 --- a/lib/mocha/parameters_matcher.rb +++ b/lib/mocha/parameters_matcher.rb @@ -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 diff --git a/test/unit/parameters_matcher_test.rb b/test/unit/parameters_matcher_test.rb index 53ffafa7..a97d2d4a 100644 --- a/test/unit/parameters_matcher_test.rb +++ b/test/unit/parameters_matcher_test.rb @@ -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