---
title: Building Custom RSpec Matchers with Regular Objects
teaser: RSpec custom matchers can be written using plain old objects.
tags: testing,ruby,rails
author: Joël Quenneville
published_on: 2023-02-01
---

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]!

[the DSL]: https://rubydoc.info/github/rspec/rspec-expectations/RSpec/Matchers#matcher-dsl
[Turns out you can]: https://rubydoc.info/github/rspec/rspec-expectations/RSpec/Matchers#custom-matcher-from-scratch

## 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.

```ruby
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:

```ruby
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!

```ruby
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.

```ruby
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.

```ruby
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! ✨

[negative assertions]: https://rubydoc.info/github/rspec/rspec-expectations/RSpec/Matchers#negated-matchers
[composable]: https://blog.arkency.com/composable-rspec-matchers/
