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!

Downloads

Posted March 11th, 2009 by Sjoerd Andringa
Tagged with: , , ,
 

Comments

 
  1. Did you know that you can already express multiple expectations in RSpec by wrapping them in a simple_matcher?

    
        def require_login
          simple_matcher "require login" do
            response.should redirect_to(login_path)
            flash[:warning].should_not be_nil
            flash[:warning].should include('please log in')
          end
        end
    

    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

    Posted March 11th, 2009 15:27 by David Chelimsky
  1. Thanks David. No, I didn’t know about the simple_matcher technique. I am fairly new to RSpec, and this is just the result of playing around with it to see what it’s all about. Somewhere in the back of my head I was sure there must be an other way of doing this, and if it wasn’t for this post I might have never found out what it was! So thanks for sharing.
    Great book btw (The RSpec Book), I am definitely going to add it to my bookshelf!

    Posted March 12th, 2009 09:37 by Sjoerd Andringa
  1. Just found out that simple_matcher doesn’t automatically call the Proc which the expectations are tested against. So you’ll have to do it explicitely:

    
    def require_login
      simple_matcher "require login" do |action|
        action.call()
        response.should redirect_to(login_path)
        flash[:warning].should_not be_nil
        flash[:warning].should include('please log in')
      end
    end
    

    Still a lot simpler than the MultiMatcher stuff I came up with though! But it is essential not to forget this, otherwise it will just test against the response of the previous test and then it can get quite confusing.

    Posted April 23rd, 2009 11:28 by Sjoerd Andringa