Custom RSpec-2 Matchers

RSpec is one of my favorite tools. I have literally fallen in love with this fantastic BDD library, especially with its second version. While using RSpec I realized it teaches me how to write tests. Yes, exactly - learning RSpec DSL, its syntax and structure of spec examples you actually learn the best practices in writing tests. RSpec, despite many built-in matchers, comes with a DSL for defining your own, custom matchers. It’s so easy that you’re not gonna believe this.

Basics

In RSpec matchers are nothing but methods available in the context of an example. You use them to make sure that a given expectation is met. There are many matchers that come with RSpec for instance here is how you can use the respond_to matcher:

describe String do
  it { should respond_to(:gsub) }
end

It’s so clean and beautiful that I probably don’t have to explain what this piece of code does, right?

Tip: When you call describe with a class as an argument, RSpec will automatically create an instance of that class and make it available via subject method. Subject is also the default context of an example block. That’s why we don’t have to write “subject.should respond_to(:gsub)”, because by default “should” or “should_not” is called on the subject.

Alright, for a list of available matchers check out the official docs. Let’s focus on writing our own matchers. If you’re wondering why you would need to do that, let me show a simple example of a User model spec:

describe User do
  before { subject.email = "foobar" }

  it "should have errors on email" do
    subject.errors.should have_key(:email)
  end

  it "should have correct error message" do
    subject.errors[:email].should include("Email is invalid")
  end
end

Now, you probably can imagine that almost identical code could be used in many other cases for many other model classes. Those 6 lines of code can be written as 1. You just need a custom matcher.

Defining a custom matcher is simple. Let’s start with a basic one that checks if a given model instance has validation errors:

RSpec::Matchers.define :have_errors_on do |attribute|
  match do |model|
    model.valid? # call it here so we don’t have to write it in before blocks
    model.errors.key?(attribute)
  end
end

And we can use it like that:

describe User do
  before { subject.email = "foobar" }

  it { should have_errors_on(:email) }
end

It covers only the first expectation, but it’s a good starting point.

Chaining

The second expectation in the example is to see if the correct validation error message is set. It’s possible to run matchers in a chain so let’s see how we can implement chaining in our custom matcher:

RSpec::Matchers.define :have_errors_on do |attribute|
  chain :with_message do |message|
    @message = message
  end

  match do |model|
    model.valid?

    @has_errors = model.errors.key?(attribute)

    if @message
      @has_errors && model.errors[attribute].include?(@message)
    else
      @has_errors
    end
  end
end

It’s really that simple. Now the matcher checks two things and returns true only if the message exists and if it matches the expected one.

Let’s use it:

describe User do
  before { subject.email = "foobar" }

  it { should have_errors_on(:email).with_message("Email has an invalid format") }
end

Nice! One line instead of six. But that’s not everything, with a failing spec failure messages might look like that:

F

Failures:

  1) User when email is not valid
     Failure/Error: it { should have_errors_on(:email).with_message("Email has an invali format") }
       expected #<user @id=nil @email="foobar"> to have errors on :email

Finished in 0.00047 seconds
1 examples, 1 failure

It’s automatically generated by RSpec based on the matcher name. It’s ok, but notice that it won’t tell us if the error message was incorrect. That’s why we need to set custom failure messages.

Custom failure messages and it’s done!

It is really recommended to use meaningful failure messages. We need to set 2 types of them, first one for “should” and second one for “should_not” expectations. The idea is that if there is an error and the message is not correct, we need to show that information in the failure output.

So, our complete matcher looks like that:

RSpec::Matchers.define :have_errors_on do |attribute|
  chain :with_message do |message|
    @message = message
  end

  match do |model|
    model.valid?

    @has_errors = model.errors.key?(attribute)

    if @message
      @has_errors && model.errors[attribute].include?(@message)
    else
      @has_errors
    end
  end

  failure_message_for_should do |model|
    if @message
      "Validation errors #{model.errors[attribute].inspect} should include #{@message.inspect}"
    else
      "#{model.class} should have errors on attribute #{attribute.inspect}"
    end
  end

  failure_message_for_should_not do |model|
    "#{model.class} should not have an error on attribute #{attribute.inspect}"
  end
end

Now if we run our example and it fails because an error message doesn’t match the expectation, we will get following failure message in the output:

F

Failures:

  1) User
     Failure/Error: it { should have_errors_on(:email).with_message("Email has an invalid format") }
       Validation errors ["Email is blah"] should include "Email has an invalid format"
     # ./_examples/rspec2_matchers.rb:55:in `block (2 levels) in <top (required)>’

Finished in 0.00053 seconds
1 example, 1 failure

Summing up

As you can see implementing custom RSpec matchers is easy trivial and it’s a highly recommended practice. There are plenty of use cases where you want to write custom matchers. It makes your specs clean and even more readable and what’s most important it keeps your spec’s code DRY and extendable.

Here are some resources if you want to learn more: