Custom Expectation Matchers in RSpec
This example will just barely scratch the surface of what you can do with custom expectation matchers. We'll dig in a little deeper down the road.
Here's a sample spec for a basic test for a contact model in an application we're building. This uses the newer rspec syntax, and should be relatively easy to read
def new_contact(options={}) Contact.new({:email=>"mike@example.com",:name=>"Mike Mangino"}.merge(options)) end describe "A new contact" do it "Should create when valid" do new_contact.should be_valid new_contact.save.should be_true end it "Should require an email address" do contact=new_contact(:email=>nil) new_contact.should_not be_valid new_contact.errors.on(:email).should_not be_nil end end
That's enough code to do a basic test of the creation of a contact. It runs, and works, but it isn't very user friendly. For instance, if you missed a required field in new_contact, you simply get an error message saying that valid? should be true, but isn't. This message comes from the default rspec expectation matcher, where if it finds a method called be_something, it sends something? to the caller and checks the result. This is slick in a lot of ways, because it lets you say things like job.should be_active without having to spend a ton of time writing custom assertions. Unfortunately, we miss the detail you get from assert_valid.
Luckily for us, we can fix this. RSpec provides an interface for building custom matchers, that let us give our own descriptions and error messages. The interface is quite simple, and consists of just a view methods. Here's an example implementation of a matcher to replace assert_valid?
class BeValid
def initialize
end
def matches?(model)
@model=model
return @model.valid?
end
def description
"be valid"
end
def failure_message
" expected to be valid, but had the following errors: #{@model.errors.full_messages.to_sentence}"
end
def negative_failure_message
" expected to not be valid, but was (missing validation?)"
end
end
def be_valid
BeValid.new
end
There are two main parts to the custom matcher. The first is a class the implements matches?, description, failure_message and optionally negative_failure_message. Matches? tells rspec whether your test passed, and the other fields are there to give descriptive messages. If you omit negative_failure_message, your code will still work, but you will not be able to say contact.should_not be_valid
The second part of the matcher is the be_valid function. This overrides the default behavior, and returns an instance of our class to be used by rspec. Once you have both of these things, you will now get much better messages from validation failures.
While that helps the error message, the syntax for the error check is a little rough. Wouldn't it be nice if we could just say contact.should have_error_on(:email)? We've got a matcher for that too!
class HaveErrorOn
def initialize(field)
@field=field
end
def matches?(model)
@model=model
@model.valid?
!@model.errors.on(@field).nil?
end
def description
"have error on #{@field}"
end
def failure_message
" expected to have error on #{@field} but doesn't"
end
end
def have_error_on(field)
HaveErrorOn.new(field)
end
This example looks very similar to the previous example. The only real difference is that the initialize method takes a parameter. That is the parameter passed to have_error_on. The more testing you do, the bigger your library of these types of matchers. We probably have 20 or 30, including some that are very specific to our applications. They make life a lot easier, and help keep your tests DRY.


