MultiMatcher for RSpec
A DRY way of asserting recurring sets of expectations with RSpec
Often I need to assert a recurring set of expectations in my specs. For instance, I frequently need to test whether an action requires the user to be logged in. To test this I’d need to do a few assertions that together describe the expected behaviour of the system when it asks the user to log in. To DRY this up a little we could put this in a separate method, but that kind of breaks with RSpec’s beautiful expressive DSL. So it would be nice if we can write something like:
@creating_a_group.should require_login
instead of:
describe "creating a group" do
it "should require login" do
post :create, :group => { :name => "My group", :description => "My description" }
response.should redirect_to(login_path)
flash[:warning].should_not be_nil
flash[:warning].should include('You need to be logged in')
end
end
Below is what I came up with after playing around with RSpec. I wrote this before I found out about RSpec’s simple_matcher, with which you can do pretty much the same thing. Still, it might inspire you to see how you can write your own matchers or how to extend RSpec.
Meet Multi Matcher
MultiMatcher (download) is a simple class which can be extended to create your own matchers that assert a set of multiple expectations. Use the expectations class method to set your expectations and optionally override failure_message and negative_failure_message.
To create a matcher that asserts the login requirement behaviour this is all you need:
module MultiMatchers
class RequireLogin < MultiMatcher
expectations do
response.should redirect_to(login_path)
flash[:warning].should_not be_nil
flash[:warning].should include('please log in')
end
end
def require_login
RequireLogin.new(self)
end
end
Just save this module (e.g. in /lib together with multi_matcher.rb) and include it in your spec environment by adding the following line to spec_helper.rb:
config.include MultiMatchers
With this in place we can use the new matcher anywhere in our specs. Just pass it to the should method on any block of code:
describe "a group" do
setup do
@creating_a_group = lambda { post :create, :group => { ... } }
@destroying_a_group = lambda { delete :destroy, :id => 1 }
end
should "require login on create" do
@creating_a_group.should require_login
end
should "require login on destroy" do
@destroying_a_group.should require_login
end
end
(Note that saving your actions to instance variables like this isn’t particularly flexible, as it doesn’t allow you to easily perform the same action with different parameters. In my next post I will introduce a trick to overcome this problem.)
Now this makes things a little more DRY in an elegant way. Please leave a comment if you found this a valuable addition to your spec tool set!

Did you know that you can already express multiple expectations in RSpec by wrapping them in a simple_matcher?
simple_matcher also supports overriding the failure messages for should or should not: http://rubyurl.com/y6CZ
That said, and I really don’t mean this to be discouraging (I love to see people extending RSpec), wrapping several expectations like this does have a couple of drawbacks. These may not be sufficient for you to want to avoid this technique, but you should be aware of them.
1. It violates the one-expectation-per-example guideline. It’s just a guideline, and nobody will arrest you for violating it, but it exists for a reason. The short version is that if the first expectation fails, you could be hiding that the subsequent expectations are also failing. Long version: http://rubyurl.com/yCt3.
2. The matcher’s name doesn’t tell you everything it does. There are three expectations here, and you have to go look in the file where you store your matchers to understand that. Sure it means you write less code in your examples, but it also makes it harder to understand when things fail.
That said, you might prefer the brevity to rapid failure isolation, localization and clarity of intent. That’s up to you
Lastly, just a heads up, while failure_message and negative_failure_message will continue to be supported (most likely forever), the next release will favor failure_message_for_should and failure_message_for_should_not. Yes the names are longer, but they are much easier to grok (negative_failure???? what was I thinking?)
Cheers,
David