Writing Beautiful RSpec Matchers

Published by Rob on September 4th, 2012 - in Development, RSpec, Ruby

thoughtbot have created a really nice set of custom matchers for RSpec.  The Shoulda matchers make writing tests for rails models beautiful and clean.

Shoulda matchers make it possible to write specs of the form:

1 2
it {should have_many(:items).through(:order_items)}
it {should have_db_column(:description).of_type(:string)}

It’s easy to do this for yourself using the friendly matcher DSL that ships with RSpec. Let’s take a look at how.

In the simplest form, all you need to do is to call the define method, passing a name, and a block which in turn calls out to match. The simple case might be:

1 2 3 4 5
RSpec::Matchers.define :be_less_than do |expected|
match do |actual|
actual<expected
end
end

We’ve just defined a matcher :be_less_than which allows us to do the following:

1 2 3
specify "5 should be less than 10" do
5.should be_less_than 10
end

If we use the describe auto subject feature of RSpec we can also write:

1 2 3
describe 10 do
it {should be_less_than 11}
end

and

1 2 3 4
describe 10 do
it {should be_less_than 11}
it {should_not be_less_than 5}
end

With that example we can see that we are getting close to the shoulda matcher syntax.  There’s still an extra level that helps make the shoulda matchers nice, chaining. Happily this is easy to do as well. The dsl provides a chain method.  The chain methods are called before the match, so you can use the chain calls to collect up additional information to validate.  Using our example above, we might want to add in an :and_greater_than chain.

1 2 3 4 5 6 7 8 9 10
RSpec::Matchers.define :be_less_than do |expected|
match do |actual|
result = actual<expected
result &&=actual>@low_value unless @low_value.nil?
result
end
chain :and_greater_than do |low_value|
@low_value=low_value
end
end

This now will give us the ability to write:

1 2 3
describe 10 do
it {should be_less_than(11).and_greater_than(5)}
end

Unfortunately the default descriptions don’t quite work as nice when there are chains in place. You’ll find that you’ll want to write a description in the matcher definition.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
RSpec::Matchers.define :be_less_than do |expected|
match do |actual|
result = actual<expected
result &&=actual>@low_value unless @low_value.nil?
result
end
chain :and_greater_than do |low_value|
@low_value=low_value
end
description do
result = "should be less than #{expected}"
result += " and greater than #{@low_value}" if @low_value
result
end
end

Unfortunately the description doesn’t automatically get used in the error messages when the matcher fails, so you’ll want to specify a failure_message_for_should. Basing it off the description is an ok starting point.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
RSpec::Matchers.define :be_less_than do |expected|
match do |actual|
result = actual<expected
result &&=actual>@low_value unless @low_value.nil?
result
end
chain :and_greater_than do |low_value|
@low_value=low_value
end
description do
result = "be less than #{expected}"
result += " and greater than #{@low_value}" if @low_value
result
end
failure_message_for_should do |actual|
"expected #{actual} "+description
end
end

Happily RSpec does the right thing with the should not description, so with the details above the matcher will be good. There might be a case where you want a different failure_message_for_should_not, which can also be specified.

That covers how to write simple reusable RSpec matchers to make your test code look as beautiful as your production code. Go ahead and try it yourself.  I’d love to see comments or questions you might have on this.

Related posts:

  1. Technique for getting JUnit tests to compile

5 Responses

  1. I’ve always wanted to write matchers but never took the time to learn the syntax. Thanks for making it simple. Here’s the matcher for JSON responses I wrote inspired by your post :)

  2. Rob says:

    Thanks for that awesome feedback Michael. It’s great to hear positive comments like this.

  3. Damien says:

    The shoulda-matchers are indeed nice. They have a big flaw though, they make you test things you shouldn’t necessarily be testing (the “have_db_column” for example).

    And some of them are quite slow and require data generation *before* the matcher, like validates_uniqueness_of.

  4. Rob says:

    @Damien – yeah — they aren’t perfect — but they do give a good direction for what rspec matchers can look like — With the simplicity of writing matchers, it’s easy to make others that really do what you want.

  5. Richard says:

    Thanks much appreciated your blog. It’s good to see how other people are writing custom matchers.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

© Rob@Rojotek