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 have_status_code
matcher.
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 failure_message
:
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
such as RestClient
, Curb
or 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
code, RestClient
uses .code
whereas Rack::Test
uses .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 elsif
for Rack::Test
and a catch-all else
that raises an UnsupportedHTTPFrameworkException
or similar.
Let’s finish up with our new failure_message
.
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…
Comments
comments powered by Disqus