You want to write a custom RSpec matcher but don’t want to fiddle with the DSL. If only you could write them using plain old objects and classes. Turns out you can!
Regular objects
In order to be used as a custom matcher, an object needs to respond to two methods:
#matches?
contains the logic for whether or not an assertion passes.#failure_message
allows you to define a helpful message for yourself when the test fails.
class DivisibleByThree
def matches?(number)
@number = number
@remainder = number % 3
@remainder == 0
end
def failure_message
"#{@number} is not cleanly divisible by 3. It has a remainder of #{@remainder}"
end
end
Using an object matcher in a test
Matcher objects can be used directly in your test assertions:
expect(79).to DivisibleByThree.new
For readability, it’s common to wrap the instantiation of the matcher object in a helper. Now it looks just like the built-in matchers!
def be_divisible_by_three
DivisibleByThree.new
end
expect(79).to be_divisible_by_three
Adding a parameter
What about matchers that take an argument? Let’s say we wanted to write a
generic divisible_by(n)
matcher? The solution is to add a constructor to
our class.
class DivisibleByN
def initialize(divisible_by)
@divisible_by = divisible_by
end
def matches?(number)
@number = number
@remainder = number % @divisible_by
@remainder == 0
end
def failure_message
"#{@number} is not cleanly divisible by #{@divisible_by}. It has a remainder of #{@remainder}"
end
end
As before, it is common to create a helper method instead of using the object directly in an assertion.
def divisible_by(n)
DivisibleByN.new(n)
end
expect(79).to be_divisible_by(10)
Adding more behavior to your matcher
And this is just the beginning!
You can extend your matcher to be used in negative assertions like
expect(x).not_to
by defining a failure_message_when_negated
method. This
is the opposite of your failure_message
.
You can make your matcher composable by including the
RSpec::Matchers::Composable
module. This means you can chain your matcher to
others using .and
or use your matcher as an argument to other matchers. Magic! ✨