Building Custom RSpec Matchers with Regular Objects

Joël Quenneville

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:

  1. #matches? contains the logic for whether or not an assertion passes.
  2. #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! ✨