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