Define your own RSpec matcher

Ruby on Rails / RSpec

RSpec built-in matchers are great but sometimes they are not enough. Fortunately, we can create easily our own matchers. Of course, we can then reuse them across the whole application. To better demonstrate why you may want to define your own matchers we would use a simple Person class which represents a user in our application. We will start in TDD way. Our requirements:

  • User should have role column with user value by the default
  • User should have verified boolean column with false value by the default
  • User should be able to upgrade to admin role and be verified automatically

We will create a plain Ruby object and focus on implementing upgrade method. Let’s name it upgrade_to_admin:

require 'spec_helper'

describe Person do
  it 'sets person role to user by the default' do
    person = Person.new

    expect(person.role).to eq('user')
  end

  it 'makes the person not verified by the default' do
    person = Person.new

    expect(person.verified).to eq(false)
  end

  describe '#upgrade_to_admin' do
    it 'upgrades to admin' do
      person = Person.new
      person.upgrade_to_admin

      expect(person.role).to eq('admin')
      expect(person.verified).to eq(true)
    end
  end
end

Let’s now satisfy our requirements by creating Person class:

class Person
  attr_reader :role, :verified

  def initialize
    @role = 'user'
    @verified = false
  end

  def upgrade_to_admin
    @role = 'admin'
    @verified = true
  end
end

Custom matchers

Let’s focus on checking if the user has given role. We would build custom matcher for this line:

expect(person.role).to eq('user')

since we are using TDD approach we have to write our expectation first:

expect(person).to be_user

I see tons of sugar syntax here. It’s not worth to create custom matcher for such simple expectation but my goal in this article is to show how we can create custom matcher and I will not focus on this matter.

Create a new file in spec/support/ directory and name it matchers.rb or however you like. We will use a little of metaprogramming here using RSpec DSL:

RSpec::Matchers.define :be_user do |expected|
  match do |actual|
    expect(actual.role).to eq('user')
  end
end

We have two variables here:

  • expected is the value passed to our matcher. Since we didn’t pass anything to be_user our variable contains nil value.
  • actual is the value passed to expect method

The last step is to require our matchers.rb file. In order to do this edit spec_helper.rb or rails_helper.rb file and use require:

require 'support/matchers'

You spec now should be green.

Updating default description

But what if we want to simplify our spec and use a one-liner:

it { expect(Person.new).to be_user }

and then we want to run spec with --format documentation flag? It would output poor description:

Person
  should be user

We can control it. We need to update our matcher definition and use description block:

RSpec::Matchers.define :be_user do |expected|
  match do |actual|
    expect(actual.role).to eq('user')
  end

  description do
    "have user role and be unverified"
  end
end

run spec again and you should see the more meaningful description:

Person
  should have user role and be unverified

That kind of documentation makes sense.

Exercise for you

You should be able now to define matchers for other test cases. I encourage you to play with custom matchers in your project and see how you can simplify more complex expectations.

Conclusion

Our matcher be_user does not say much someone who doesn’t know the implementation. However, what matters is the documentation output. If someone would want to familiarize with the project he would run the tests first instead of looking at the implementation of the test.