freerange / mocha

A mocking and stubbing library for Ruby

Home Page:https://mocha.jamesmead.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`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?

def with(*expected_parameters_or_matchers, &matching_block)
@parameters_matcher = ParametersMatcher.new(expected_parameters_or_matchers, self, &matching_block)
self
end

class ParametersMatcher
def initialize(expected_parameters = [ParameterMatchers::AnyParameters.new], expectation = nil, &matching_block)
@expected_parameters = expected_parameters
@expectation = expectation
@matching_block = matching_block
end
def match?(actual_parameters = [])
if @matching_block
@matching_block.call(*actual_parameters)
else
parameters_match?(actual_parameters)
end
end

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 🅰️ would be better.

@sambostock

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 🅰️ or 🅱️.

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 in all_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.