Creating a custom “be_valid” matcher for rspec_on_rails

I’ve been discovering the excellent ‘behaviour-driven development’ plugin ‘rspec’ , and its rails-specific cousin (nephew?) ‘rspec_on_rails’ this week.

At the moment i’m testing that validation works properly on some models. Now, rspec already generates a matcher for any predicate method that can be called on an object (eg “object.should be_foo” for “ eql(true)”). I was using this with valid?, which we use on an typical (ie ActiveRecord) model to see if it will pass validation (and therefore be able to be saved to the database). This is generally nicer than trying to save it and seeing if it saved ok.

So, i was writing specs like this

it "should validate with only a name" do
  @foo = => "bar")
  @foo.should be_valid

This works, in terms of correctly passing or failing, but it doesn’t give me much information – it just tells me that something fails validation. I could get at the error messages in the test by doing the test as follows:

it "should validate with only a name" do
  @foo = => "bar")
  @foo.should be_valid
  @foo.errors.full_messages.should eql([])

but this is a bit tiresome. I’d rather just write “should be_valid”. So, i decided to write a custom matcher that overrides the auto-generated ‘be_valid’ method and tells me why it failed validation (if it does fail). Here’s how i did it. Thanks to the excellent peepcode rspec movies for the inspiration and knowhow!In my spec folder, i made a file called ‘custom_matcher.rb’. In it is a module which will hold any custom rspec matchers i write, though there’s only one at the moment.

module CustomMatchers
  #checks that an AR model validates, by testing error messages from .valid?
  #displays any error messages recieved in test failure output
  class BeValid
    #do any setup required - at the very least, set some instance variables.
    #In this case, i don't take any arguments - it simply either passes or fails.
    def initialize
      @expected = []

    #perform the actual match - 'target' is the thing being tested
    def matches?(target)
      #target.errors.full_messages is an array of error messages produced by the valid? method
      #if valid? is true, it will be empty
      @errors = target.errors.full_messages

    #displayed when 'should' fails
    def failure_message
      "validation failed with #{@errors.inspect}, expected no validation errors"

    #displayed when 'should_not' fails
    def negative_failure_message
      "validation succeeded, expected one or more validation errors"

    #displayed in the spec description if the user doesn't provide one (ie if they just write 'it do' for the spec header)
    def description
      "validate successfully"

    # Returns string representation of the object being tested
    def to_s(value)

  # the matcher method that the user calls in their specs
  def be_valid
#To hook it up, add the following require to spec_helper.rb:
require 'spec/custom_matchers'
#And add the following line to the "Spec::Runner.configure do |config|" section:

This is about as simple as a matcher could be – i don’t take any arguments at all, so the expected result is always an empty array (of errors). The ‘value’ added by this is that i’m now outputting the error messages (if any) in the failure report. Because i’ve provided a description method, i don’t even need to provide a description to my specs if i don’t want to: for example, here’s a test which will fail (and its surrounding description):

describe SchoolSubscriber, ", when we make an empty school_subscriber, " do
  before do
    @ss =

  it do
    @ss.should be_valid

And here’s the failure report from rspec:

'SchoolSubscriber , when we make an empty school_subscriber,  should validate successfully' FAILED
validation failed with ["Phone number can't be blank", "First name can't be blank", "Position can't be blank", "School can't be blank", "Last name can't be blank", "Email can't be blank"], expected no validation errors

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s