RSpec brings Ruby's readability to testing. Custom matchers take it to the next level 🚀.

I need to write test checking if a token is in certain format. For the sake of simplicity, let us consider the UUID format (Hex 8-4-4-4-12). A simple test would be:

it "has UUID token" do
  expect(token).to match(/^\h{8}-(\h{4}-){3}\h{12}$/)
end

Little refactoring:

UUID_FORMAT = /^\h{8}-(\h{4}-){3}\h{12}$/.freeze

it "has UUID token" do
  expect(token).to match(UUID_FORMAT)
end

But we can do better with a custom matcher:

require 'rspec/expectations'

UUID_FORMAT = /^\h{8}-(\h{4}-){3}\h{12}$/.freeze

RSpec::Matchers.define :be_a_uuid do |uuid_format: UUID_FORMAT|
  match do |actual|
    actual.match(uuid_format)
  end
end

Put above custom matcher into a support helper and our test becomes:

it "has UUID token" do
  expect(token).to be_a_uuid
end

Much better! 🎉

Final touch is to customize failure messages to be more readable:

require 'rspec/expectations'

UUID_FORMAT = /^\h{8}-(\h{4}-){3}\h{12}$/.freeze

RSpec::Matchers.define :be_a_uuid do |uuid_format: UUID_FORMAT|
  match do |actual|
    actual.match(uuid_format)
  end

  failure_message do |actual|
    "expected #{actual} to be a UUID"
  end

  # used for negation: `expect(token)not.to be_a_uuid`
  failure_message_when_negated do |actual|
    "expected #{actual} not to be a UUID"
  end
end

And just like all other code, let us test this custom matcher:

require 'securerandom'
require 'rspec/matchers/fail_matchers'
RSpec.configure do |config|
  config.include RSpec::Matchers::FailMatchers
end

RSpec.describe 'UUID matcher' do
  # The matcher works
  it 'matches with UUID' do
    expect(SecureRandom.uuid).to be_a_uuid
  end

  it 'does not match with plain string' do
    expect('foobar').not_to be_a_uuid
  end

  # Checking failure messages
  it 'fails with UUID' do
    string = SecureRandom.uuid

    expect { expect(string).not_to be_a_uuid }
      .to fail_with("expected #{string} not to be a UUID")
  end

  it 'fails with plain string' do
    string = "foobar"

    expect { expect(string).to be_a_uuid }
      .to fail_with("expected foobar to be a UUID")
  end
end

Notice how RSpec nicely exposes the fail_with matcher to check the failure messages.

You can find full running example in this gist.

More details about custom matchers are in the RSpec documentation.