RSpec: Composing a custom matcher from existing matchers

Updated . Posted . Visible to the public.

When you find similar groups of expect calls in your tests, you can improve readability by extracting the group into its own matcher. RSpec makes this easy by allowing matchers to call other matchers.

Example

The following test checks that two variables foo and bar (1) have no lowercase characters and (2) end with an exclamation mark:

expect(foo).to_not match(/[a-z]/)
expect(foo).to end_with('!')

expect(bar).to_not match(/[a-z]/)
expect(bar).to end_with('!')

We can extract the repeated matcher chains into a custom matcher called be_shouting:

expect(foo).to be_shouting
expect(bar).to be_shouting

Instead of re-implementing the match and end_with matchers, our new matcher can simply use expect:

RSpec::Matchers.define :be_shouting do
  
  match do |string|
    expect(string).to_not match(/[a-z]/)
    expect(string).to end_with('!')
    true
  end
  
end

Tip

It's a good habit to always end a composed matcher in true to ensure that it returns a truthy value. E.g. by using a condition as the last expression you may otherwise implement a matcher that sometimes fails:

match do |string|
  if some_condition
    expect(string).to_not match(/[a-z]/)
    expect(string).to end_with('!')
  end
 # ❌ matcher is falsy when the `if` branch is not taken
end

Revealing more details in failure messages

When an embedded matcher fails (like end_with('!') in the example), its failure message will be overridden by the custom matcher:

Failure/Error: expect("FOO").to be_shouting
  expected "FOO" to be shouting

You may prefer to see the exact message produced by the failing matcher:

Failure/Error: expect("FOO").to be_shouting
  expected "FOO" to end with "!"

You can do this by passing a { notify_expectation_failures: true } option to your custom matcher's match block:

RSpec::Matchers.define :be_shouting do
  
  match(notify_expectation_failures: true) do |string|
    expect(string).to_not match(/[a-z]/)
    expect(string).to end_with('!')
    true
  end
  
end
Henning Koch
Last edit
Henning Koch
License
Source code in this card is licensed under the MIT License.
Posted by Henning Koch to makandra dev (2023-11-24 12:44)