Defining a custom matcher
rspec-expectations provides a DSL for defining custom matchers. These are often useful for expressing expectations in the domain of your application.
Behind the scenes RSpec::Matchers.define
evaluates the define
block in the context of a singleton class. If you need to write a more complex matcher and would like to use the Class
-approach yourself, please head over to our API
-documentation and read the docs about the MatcherProtocol
.
Define a matcher with default messages
Given a file named "matcher_with_default_message_spec.rb" with:
require 'rspec/expectations'
RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
end
RSpec.describe 9 do
it { is_expected.to be_a_multiple_of(3) }
end
RSpec.describe 9 do
it { is_expected.not_to be_a_multiple_of(4) }
end
# fail intentionally to generate expected output
RSpec.describe 9 do
it { is_expected.to be_a_multiple_of(4) }
end
# fail intentionally to generate expected output
RSpec.describe 9 do
it { is_expected.not_to be_a_multiple_of(3) }
end
When I run rspec ./matcher_with_default_message_spec.rb --format documentation
Then the exit status should not be 0
And the output should contain "is expected to be a multiple of 3"
And the output should contain "is expected not to be a multiple of 4"
And the output should contain "Failure/Error: it { is_expected.to be_a_multiple_of(4) }"
And the output should contain "Failure/Error: it { is_expected.not_to be_a_multiple_of(3) }"
And the output should contain "4 examples, 2 failures"
And the output should contain "expected 9 to be a multiple of 4"
And the output should contain "expected 9 not to be a multiple of 3".
Overriding the failure_message
Given a file named "matcher_with_failure_message_spec.rb" with:
require 'rspec/expectations'
RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
failure_message do |actual|
"expected that #{actual} would be a multiple of #{expected}"
end
end
# fail intentionally to generate expected output
RSpec.describe 9 do
it { is_expected.to be_a_multiple_of(4) }
end
When I run rspec ./matcher_with_failure_message_spec.rb
Then the exit status should not be 0
And the stdout should contain "1 example, 1 failure"
And the stdout should contain "expected that 9 would be a multiple of 4".
Overriding the failure_message_when_negated
Given a file named "matcher_with_failure_for_message_spec.rb" with:
require 'rspec/expectations'
RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
failure_message_when_negated do |actual|
"expected that #{actual} would not be a multiple of #{expected}"
end
end
# fail intentionally to generate expected output
RSpec.describe 9 do
it { is_expected.not_to be_a_multiple_of(3) }
end
When I run rspec ./matcher_with_failure_for_message_spec.rb
Then the exit status should not be 0
And the stdout should contain "1 example, 1 failure"
And the stdout should contain "expected that 9 would not be a multiple of 3".
Overriding the description
Given a file named "matcher_overriding_description_spec.rb" with:
require 'rspec/expectations'
RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
description do
"be multiple of #{expected}"
end
end
RSpec.describe 9 do
it { is_expected.to be_a_multiple_of(3) }
end
RSpec.describe 9 do
it { is_expected.not_to be_a_multiple_of(4) }
end
When I run rspec ./matcher_overriding_description_spec.rb --format documentation
Then the exit status should be 0
And the stdout should contain "2 examples, 0 failures"
And the stdout should contain "is expected to be multiple of 3"
And the stdout should contain "is expected not to be multiple of 4".
With no args
Given a file named "matcher_with_no_args_spec.rb" with:
require 'rspec/expectations'
RSpec::Matchers.define :have_7_fingers do
match do |thing|
thing.fingers.length == 7
end
end
class Thing
def fingers; (1..7).collect {"finger"}; end
end
RSpec.describe Thing do
it { is_expected.to have_7_fingers }
end
When I run rspec ./matcher_with_no_args_spec.rb --format documentation
Then the exit status should be 0
And the stdout should contain "1 example, 0 failures"
And the stdout should contain "is expected to have 7 fingers".
With multiple args
Given a file named "matcher_with_multiple_args_spec.rb" with:
require 'rspec/expectations'
RSpec::Matchers.define :be_the_sum_of do |a,b,c,d|
match do |sum|
a + b + c + d == sum
end
end
RSpec.describe 10 do
it { is_expected.to be_the_sum_of(1,2,3,4) }
end
When I run rspec ./matcher_with_multiple_args_spec.rb --format documentation
Then the exit status should be 0
And the stdout should contain "1 example, 0 failures"
And the stdout should contain "is expected to be the sum of 1, 2, 3, and 4".
With a block arg
Given a file named "matcher_with_block_arg_spec.rb" with:
require 'rspec/expectations'
RSpec::Matchers.define :be_lazily_equal_to do
match do |obj|
obj == block_arg.call
end
description { "be lazily equal to #{block_arg.call}" }
end
RSpec.describe 10 do
it { is_expected.to be_lazily_equal_to { 10 } }
end
When I run rspec ./matcher_with_block_arg_spec.rb --format documentation
Then the exit status should be 0
And the stdout should contain "1 example, 0 failures"
And the stdout should contain "is expected to be lazily equal to 10".
With helper methods
Given a file named "matcher_with_internal_helper_spec.rb" with:
require 'rspec/expectations'
RSpec::Matchers.define :have_same_elements_as do |sample|
match do |actual|
similar?(sample, actual)
end
def similar?(a, b)
a.sort == b.sort
end
end
RSpec.describe "these two arrays" do
specify "should be similar" do
expect([1,2,3]).to have_same_elements_as([2,3,1])
end
end
When I run rspec ./matcher_with_internal_helper_spec.rb
Then the exit status should be 0
And the stdout should contain "1 example, 0 failures".
Scoped in a module
Given a file named "scoped_matcher_spec.rb" with:
require 'rspec/expectations'
module MyHelpers
extend RSpec::Matchers::DSL
matcher :be_just_like do |expected|
match {|actual| actual == expected}
end
end
RSpec.describe "group with MyHelpers" do
include MyHelpers
it "has access to the defined matcher" do
expect(5).to be_just_like(5)
end
end
RSpec.describe "group without MyHelpers" do
it "does not have access to the defined matcher" do
expect do
expect(5).to be_just_like(5)
end.to raise_exception
end
end
When I run rspec ./scoped_matcher_spec.rb
Then the stdout should contain "2 examples, 0 failures".
Scoped in an example group
Given a file named "scoped_matcher_spec.rb" with:
require 'rspec/expectations'
RSpec.describe "group with matcher" do
matcher :be_just_like do |expected|
match {|actual| actual == expected}
end
it "has access to the defined matcher" do
expect(5).to be_just_like(5)
end
describe "nested group" do
it "has access to the defined matcher" do
expect(5).to be_just_like(5)
end
end
end
RSpec.describe "group without matcher" do
it "does not have access to the defined matcher" do
expect do
expect(5).to be_just_like(5)
end.to raise_exception
end
end
When I run rspec scoped_matcher_spec.rb
Then the output should contain "3 examples, 0 failures".
Matcher with separate logic for expect().to and expect().not_to
Given a file named "matcher_with_separate_should_not_logic_spec.rb" with:
RSpec::Matchers.define :contain do |*expected|
match do |actual|
expected.all? { |e| actual.include?(e) }
end
match_when_negated do |actual|
expected.none? { |e| actual.include?(e) }
end
end
RSpec.describe [1, 2, 3] do
it { is_expected.to contain(1, 2) }
it { is_expected.not_to contain(4, 5, 6) }
# deliberate failures
it { is_expected.to contain(1, 4) }
it { is_expected.not_to contain(1, 4) }
end
When I run rspec matcher_with_separate_should_not_logic_spec.rb
Then the output should contain all of these:
| 4 examples, 2 failures | | expected [1, 2, 3] to contain 1 and 4 | | expected [1, 2, 3] not to contain 1 and 4 |
Use define_method to create a helper method with access to matcher params
Given a file named "define_method_spec.rb" with:
RSpec::Matchers.define :be_a_multiple_of do |expected|
define_method :is_multiple? do |actual|
actual % expected == 0
end
match { |actual| is_multiple?(actual) }
end
RSpec.describe 9 do
it { is_expected.to be_a_multiple_of(3) }
it { is_expected.not_to be_a_multiple_of(4) }
# deliberate failures
it { is_expected.to be_a_multiple_of(2) }
it { is_expected.not_to be_a_multiple_of(3) }
end
When I run rspec define_method_spec.rb
Then the output should contain all of these:
| 4 examples, 2 failures | | expected 9 to be a multiple of 2 | | expected 9 not to be a multiple of 3 |
Include a module with helper methods in the matcher
Given a file named "include_module_spec.rb" with:
module MatcherHelpers
def is_multiple?(actual, expected)
actual % expected == 0
end
end
RSpec::Matchers.define :be_a_multiple_of do |expected|
include MatcherHelpers
match { |actual| is_multiple?(actual, expected) }
end
RSpec.describe 9 do
it { is_expected.to be_a_multiple_of(3) }
it { is_expected.not_to be_a_multiple_of(4) }
# deliberate failures
it { is_expected.to be_a_multiple_of(2) }
it { is_expected.not_to be_a_multiple_of(3) }
end
When I run rspec include_module_spec.rb
Then the output should contain all of these:
| 4 examples, 2 failures | | expected 9 to be a multiple of 2 | | expected 9 not to be a multiple of 3 |
Using values_match? to compare values and/or compound matchers.
Given a file named "compare_values_spec.rb" with:
RSpec::Matchers.define :have_content do |expected|
match do |actual|
# The order of arguments is important for `values_match?`, e.g.
# especially if your matcher should handle `Regexp`-objects
# (`/regex/`): First comes the `expected` value, second the `actual`
# one.
values_match? expected, actual
end
end
RSpec.describe 'a' do
it { is_expected.to have_content 'a' }
end
RSpec.describe 'a' do
it { is_expected.to have_content /a/ }
end
RSpec.describe 'a' do
it { is_expected.to have_content a_string_starting_with('a') }
end
When I run rspec ./compare_values_spec.rb --format documentation
Then the exit status should be 0.
Error handling
Make sure your matcher returns either true
or false
. Take care to handle exceptions appropriately in your matcher, e.g. most cases you might want your matcher to return false
if an exception - e.g. ArgumentError - occurs, but there might be edge cases where you want to pass the exception to the user.
You should handle each StandardError
with care! Do not handle them all in one.
match do |actual|
begin
'[...] Some code'
rescue ArgumentError
false
end
end
Given a file named "error_handling_spec.rb" with:
class CustomClass; end
RSpec::Matchers.define :is_lower_than do |expected|
match do |actual|
begin
actual < expected
rescue ArgumentError
false
end
end
end
RSpec.describe 1 do
it { is_expected.to is_lower_than 2 }
end
RSpec.describe 1 do
it { is_expected.not_to is_lower_than 'a' }
end
RSpec.describe CustomClass do
it { expect { is_expected.not_to is_lower_than 2 }.to raise_error NoMethodError }
end
When I run rspec ./error_handling_spec.rb --format documentation
Then the exit status should be 0.
Define aliases for your matcher
If you want your matcher to be readable in different contexts, you can use the .alias_matcher
-method to provide an alias for your matcher.
Given a file named "alias_spec.rb" with:
RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
end
RSpec::Matchers.alias_matcher :be_n_of , :be_a_multiple_of
RSpec.describe 9 do
it { is_expected.to be_n_of(3) }
end
When I run rspec ./alias_spec.rb --format documentation
Then the exit status should be 0.
With expectation errors that bubble up
By default the match block will swallow expectation errors (e.g. caused by using an expectation such as expect(1).to eq 2
), if you wish to allow these to bubble up, pass in the option :notify_expectation_failures => true
.
Given a file named "bubbling_expectation_errors_spec.rb" with:
RSpec::Matchers.define :be_a_palindrome do
match(:notify_expectation_failures => true) do |actual|
expect(actual).to be_a(String)
expect(actual.reverse).to eq(actual)
end
end
RSpec.describe "a custom matcher that bubbles up expectation errors" do
it "bubbles expectation errors" do
expect("carriage").to be_a_palindrome
end
end
When I run rspec bubbling_expectation_errors_spec.rb
Then the output should contain all of these:
| Failures: | | 1) a custom matcher that bubbles up expectation errors bubbles expectation errors | | Failure/Error: expect(actual.reverse).to eq(actual) | | expected: "carriage" | | got: "egairrac" | | (compared using ==) | | # ./bubbling_expectation_errors_spec.rb:4 | | # ./bubbling_expectation_errors_spec.rb:10 |