Skip to main content

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 |