Writing elegant custom matchers in RSpec
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.