custom expectation matchers

February 18th, 2007

RSpec 0.8 introduces the concept of Expectation Matchers to RSpec. Not only does this simplify RSpec’s own internals, but it also makes it really simple to write your own custom expectation matchers.

Here’s an example that came from a question on the rspec-devel mailing list.

Evgeny wanted a simple, DRY way to specify that a Rails model class should require specific fields. Here’s what I came up with. I’m not convinced this is the best approach to this problem, but I’m presenting it here to demonstrate the simplicity of creating a custom matcher.

module ModelSpecHelper
  class Require
    def initialize(attr)
      @attr = attr
    end

    def matches?(model)
      @model = model
      model.send("#{@attr.to_s}=".to_sym, nil)
      return !model.valid?
    end

    def failure_message
      "expected #{@model.inspect} to require #{@attr.inspect}" 
    end
  end

  def require(attr)
    Require.new(attr)
  end
end

context "User behaviour" do
  include ModelSpecHelper

  setup do
    @user = User.new(:email => 'a@b.com', :zip => '02134')
  end

  specify "should require email" do
    @user.should require(:email)
  end

  specify "should require zip" do
    @user.should require(:zip)
  end
end

The only thing I’m not comfortable with is that these specs are bound together with the setup. If you don’t initialize the required attributes in setup and only one of the required attributes is actually implemented in the model, all of them will pass anyway (false positive).

I guess that could be solved by adding a parameter to #require:

def require(attr, valid_value)
  Require.new(attr, valid_value)
end

Then #matches? could assign nil to the attr and expect #valid? to return false and then assign valid_value to the attr and expect #valid? to return true. The failure message could say something like “model was not valid even when email was assigned ‘a@b.com’. You must ensure that all required attributes are assigned values before calling #should require.”

Making that change would be cake! Again, the point here is to show how easy custom matchers are to implement. Coming up w/ the right syntax and messaging is another problem, but RSpec does its best to stay out of your way as you explore that problem.

So I encourage you to explore custom matchers if you’re using RSpec >= 0.8, and I look forward to hearing about your experiences doing so. I also encourage you to consider publishing your own libraries of custom matchers that you find generally useful.

4 Responses to “custom expectation matchers”

  1. Tom
    Tom Says:

    Seems like you would just check #errors.on() instead of #valid?.

  2. Tom
    Tom Says:

    This works for me:

        def matches?(model)
          @model = model
          model.send("#{@attr.to_s}=".to_sym, nil)
          model.valid?
          return !model.errors.on("#{@attr.to_s}".to_sym).nil?
        end
  3. David Chelimsky
    David Chelimsky Says:

    For that matter you can just use errors_on, which is an AR::Base extension already present in the plugin:

    model.should have(n).errors_on(:attr)

    It calls valid? and returns errors.on, and makes it accessible through the Have matcher.

  4. Tom
    Tom Says:

    Nice, thanks.

Leave a Reply (Textile Enabled)