Defining Custom Matchers in RSpec
Defining custom RSpec matchers is really easy and a great way to increase the readability of your tests.
Using your own matcher is a much better option rather than trying to retrofit RSpec’s built-in matchers to fit your individual use case.
A retro-fitted example
You may want to check that a HTTP response has a particular status code. Using RSpec’s built-in matchers would look something like this:
But it would read much better with a custom
Defining a custom matcher
It’s really easy to do this.
Ultimately, all we’re really checking is that the status code of a HTTP request returns a certain value.
Providing a custom error message
We can improve our matcher further with a custom exception message. This is where the usefulness of writing your own
matcher really comes out, as it provides an exact, bespoke error message rather than something generic like
"expected false but got true" which we’ve all experienced at some point.
Simply extend the matcher above with a
When this fails, the error looks like this:
Which is useful as it adds more context to the test failure.
Extending our custom matcher further
Our custom matcher does the job but there are some potential problems with it.
Perhaps you are using more than one HTTP framework in your tests or - more likely - you are using the
Rack::Test framework for unit-testing Sinatra apps as well as an HTTP framework
HTTParty for integration or acceptance tests for example.
In such cases it would be a good idea to use the same custom matcher defined above for all cases
(DRY). However, the APIs can differ e.g. to return the status
Let’s harness the power of Ruby’s metaprogramming using
respond_to? to handle this.
Of course, the more HTTP frameworks you have the more complexity is introduced.
It is probably a good idea to tighten up that
if statement with an
Rack::Test and a catch-all
that raises an
UnsupportedHTTPFrameworkException or similar.
Let’s finish up with our new
To provide a single error message we needed to introduce the
status_code variable and ensure it was at a scope that
made it available to the
failure_message block. This gave us the opportunity to use the much terser ternary operator
and split out the fetching of the status code from the matcher comparison.
May your tests now be more readable…