Monitoring Specific Messages in RSpec Part 3

Check out the full Monitoring Messages in Rspec series: part 1, part 2, part 3

Continuing the series ( part 1, part 2 ) on matching messages in RSpec, the next logical step is custom argument matchers.

1
expect(mechanic).to receive(:fix).with something_broken

Using the RSpec matcher DSL this could simply look like:

1
2
3
4
5
RSpec::Matchers.define :something_broken do
  match do |thing|
    thing.broken?
  end
end

That’s all there is to it. Now it can be used as both a regular matcher and as an argument matcher.

If the matcher needs to be created from scratch, a matches? method must be defined instead:

1
2
3
4
5
6
7
8
9
class SomethingBroken
  def matches?(target)
    target.broken?
  end
end

def something_broken
  SomethingBroken.new
end

This works just fine as a normal matcher, however, when used as an argument matcher, it will always fail. The reason is that argument matchers are invoked with the == operator, which by default, verifies if the objects are the same object.

Attempting to use a normal matcher with the change expectation also oddly fails, due to change invoking the === message, not matches?. Since the default === behavior is ==, the existing argument matchers currently work with it.

There is active talk / changes happening to standardize the matchers to ===. This will allow for a more consistent and composable interface. It also has the added benefit of allowing the matchers to be used in more complex conditionals using case statements.

To fix the class based matcher simply add the necessary alias(es):

1
2
3
4
5
6
7
class SomethingBroken
  def matches?(target)
    target.broken?
  end
  alias_method :==, :matches?
  alias_method :===, :matches?  # Not technically necessary due to default ==
end

Note that with such a simple matcher, there is no reason it cannot be created as a simple composed method using an existing matcher:

1
2
3
def something_broken
  be_broken   # Note: Not all built in matchers have == aliases yet
end