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 “object.foo?.should 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 = Foo.new(:name => "bar")
@foo.should be_valid
end
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 = Foo.new(:name => "bar")
@foo.should be_valid
@foo.errors.full_messages.should eql([])
end
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 = []
end
#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
target.valid?
@errors = target.errors.full_messages
@errors.eql?(@expected)
end
#displayed when 'should' fails
def failure_message
"validation failed with #{@errors.inspect}, expected no validation errors"
end
#displayed when 'should_not' fails
def negative_failure_message
"validation succeeded, expected one or more validation errors"
end
#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"
end
# Returns string representation of the object being tested
def to_s(value)
"#{@errors.inspect}"
end
end
# the matcher method that the user calls in their specs
def be_valid
BeValid.new
end
end
#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:
config.include(CustomMatchers)
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 = SchoolSubscriber.new
end
it do
@ss.should be_valid
end
end
And here’s the failure report from rspec:
1)
'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