`with(...) { ... }` should match both given args and block, or reject both
sambostock opened this issue · comments
Context
Consider the following code
something.expects(:a_method).with(instance_of(Foo)) do |arg|
# check something complicated about arg
end
Currently, only the block matcher will be checked, and instance_of(Foo)
will be ignored.
This is because specifying expected_parameters
and a matching_block
are silently mutually exclusive. We can see this by following the code to ParametersMatcher#match?
mocha/lib/mocha/expectation.rb
Lines 269 to 272 in f7e1763
mocha/lib/mocha/parameters_matcher.rb
Lines 5 to 18 in f7e1763
match?
checks if a block was provided, and if so matches against that. It only matches against the expected_parameters
if no block was given.
This means it is possible to write the following passing test:
something.expects(:a_method).with(all_of(instance_of(TrueClass), instance_of(FalseClass))) { true }
something.a_method("not a boolean at all")
Proposal
🅰️ Match both
Match against the parameter matchers and the block. Something like:
def match?(actual_parameters = [])
matches_block?(actual_parameters) && parameters_match?(actual_parameters)
end
def matches_block?(actual_parameters)
return true if @matching_block.nil?
@matching_block.call(*actual_parameters)
end
🅱️ Make mutual exclusion explicit
Raise if passed both expected parameters and a block are given
def with(*expected_parameters_or_matchers, &matching_block)
raise ArgumentError, "params and block are mutually exclusive" if matching_block && !expected_parameters_or_matchers.empty?
@parameters_matcher = ParametersMatcher.new(expected_parameters_or_matchers, self, &matching_block)
self
end
Opinion
My gut feeling is
Well spotted and thanks for reporting - I'm amazed someone hasn't come across that before!
I agree with that option A would be better / less surprising. Either way, Mocha will need to display a deprecation warning before the behaviour is changed.
Your question has also made me wonder whether calling Expectation#with
ought to combine matching conditions as well, but that's for another day!
Yeah, I considered that this would be a potentially breaking change. I figured a deprecation + config option to opt-in would be the way to go, regardless of
whether calling
Expectation#with
ought to combine matching conditions as well
🤔 I'm not sure I follow what you mean here? Do you mean that with
should wrap the args in all_of
? I'm guessing not, because that would break multiple parameters.
whether calling
Expectation#with
ought to combine matching conditions as well🤔 I'm not sure I follow what you mean here? Do you mean that
with
should wrap the args inall_of
? I'm guessing not, because that would break multiple parameters.
Sorry I missed a crucial bit in that sentence - it should've read:
calling
Expectation#with
multiple times ought to combine matching conditions
e.g. something.expects(:a_method).with(has_key(:foo)).with(has_key(:bar))
# => matches something.a_method({foo: 1, bar: 2})
but not something.a_method({foo: 1})
Having written it out more explicitly, perhaps it's unlikely that someone would do it, but it is technically possible. A bit of a hazard with a chain-able API, I guess.