homeASCIIcasts

157: RSpec Matchers and Macros 

(view original Railscast)

This episode continues the recent series on testing and in it we’ll be showing you how to create custom matchers and macros in RSpec. Matchers and macros can be used to refactor your specs and we’ll be using them to clean up some existing specs for an application.

Custom Matchers

To demonstrate custom matchers we will modify the second of the two specs below.

require File.dirname(__FILE__) + '/../spec_helper'
describe ArticlesController do
  it "should not set position when not saved to database" do
    article = Article.new(:name => "Foo")
    article.position.should be_nil
  end
  it "should increment article position when creating next article" do
    article1 = Article.create!(:name => "Foo")
    article2 = Article.create!(:name => "Bar")
    article2.position.should == article1.position + 1
  end
end

In RSpec a matcher is anything that appears to the right of the words should or should_not. The first spec above has the matcher be_nil while the second one uses ==. Matchers are equivalent to the assertions used by Test::Unit in that they compare an expected value with an actual one.

Creating Our First Custom Matcher

In the second spec above we create two articles and check that the position of the second is one greater than that of the first. Instead of using == article1.position + 1 we’re going to create a custom matcher called be_one_more_than and use that to check the second article’s position against the first. We’ll start by using the new matcher, then define it.

it "should increment article position when creating next article" do
  article1 = Article.create!(:name => "Foo")
  article2 = Article.create!(:name => "Bar")
  article2.position.should be_one_more_than(article1.position)
end

Using our custom matcher.

There are two ways that we can define a custom matcher. The first is to write a simple matcher, where we just define a method; the second is to create a new class to describe the matching behaviour.

Writing a Simple Matcher

We’ll write be_one_more_than as a simple matcher method. Any method defined in a describe block is available to all of the specs in that block so we can create the method there.

describe ArticlesController do
  def be_one_more_than(number)
    simple_matcher("one more than #{number}") { |actual| actual == number + 1 }
  end
  # (specs here)
end

RSpec provides a simple_matcher method to enable us to write our own simple matchers. The method takes two arguments. The first is a description which will be shown if match fails; the second is a block which takes the actual value we’re testing (the value before the word should) and returns a boolean value depending on whether the comparison defined in the block passes. The block for our method returns true if the actual value is one more than the expected value.

If we rerun our specs they still pass, so our matcher is working.

$ rake spec
(in /Users/eifion/rails/apps_for_asciicasts/ep157)
...........
Finished in 0.578004 seconds
11 examples, 0 failures

Making The Matcher Available To All Specs

While the simple matcher works, but it is only available to the specs in one file. If we want to create custom matchers that work in any of our specs we can do so by defining them in a module and making that module available to RSpec.

To do this we’ll create a file in our application’s spec directory and called custom_matchers.rb. In that file we’ll create a module and move our be_one_more_than method into it.

module CustomMatcher
  def be_one_more_than(number)
    simple_matcher("one more than #{number}") { |actual| actual == number + 1 }
  end
end

Next we have to modify the spec_helper.rb file in the spec directory so that RSpec knows about our new module. To do this we have to add a new require statement so that our custom_modifiers file is referenced, and then configure RSpec to include our new module.

# This file is copied to ~/spec when you run 'ruby script/generate rspec'
# from the project root directory.
ENV["RAILS_ENV"] ||= 'test'
require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
require 'spec/autorun'
require 'spec/rails'
require File.dirname(__FILE__) + "/custom_matchers"
Spec::Runner.configure do |config|
  config.use_transactional_fixtures = true
  config.use_instantiated_fixtures  = false
  config.fixture_path = RAILS_ROOT + '/spec/fixtures/'
  config.mock_with :mocha
  config.include(CustomMatcher)
end

All of the methods in the CustomMatcher module will now be available to all spec descriptions, so we can remove the be_one_more_than method from the article_spec file.

Our custom matcher should work with both should and should_not. To test it we’ll change the spec we used earlier to use should_not to check that the custom matcher works and to see what error message is shown when the match fails.

article2.position.should_not be_one_more_than(article1.position)

Now we’ll run the specs again…

$ rake spec
(in /Users/eifion/rails/apps_for_asciicasts/ep157)
..........F
1)
'ArticlesController should increment article position when creating next article' FAILED
expected not to get "one more than 1", but got 2
./spec/models/article_spec.rb:12:
Finished in 0.442849 seconds
11 examples, 1 failure

…and, as expected, we get one failure as we’ve reversed the criteria of one of the specs.

Creating a Matcher Class

The error message that the failing spec displays makes sense if you read it carefully enough, but it’s a little cryptic. While it’s possible to create different messages for simple matchers, it’s a little complicated to implement, so instead we’ll turn our simple matcher into a full RSpec matcher class.

A matcher is an object that responds to the methods matches? and failure_message_for_should. It can also use the optional methods does_not_match?, failure_message_for_should_not and description. We’ll create a class in our CustomMatchers module to replace the simple matcher we created earlier. (By the way, if you’re using TextMate and have the RSpec bundle installed you can type matc and TAB and an example class will be generated for you.)

Our matcher class will look like this:

module CustomMatcher
  class OneMoreThan
    def initialize(expected)
      @expected = expected
    end
    def matches?(actual)
      @actual = actual
      @actual == @expected + 1
    end
    def failure_message_for_should
      "expected #{@actual.inspect} to be one more than #{@expected.inspect}"
    end
    def failure_message_for_should_not
      "expected #{@actual.inspect} not to be one more than #{@expected.inspect}"
    end
  end
  def be_one_more_than(expected)
    OneMoreThan.new(expected)
  end
end

The module still has the be_one_more_than method that we used in our simple matcher, but now instead of returning a simple_matcher it returns an instance of our new OneMoreThan class. The expected value is passed to the class’s constructor where it is stored as an instance variable. The actual work is done in the matches? method which is the method that RSpec calls when the spec is run. This method can be defined in two ways: we can either have a should expectation in the method (in our case this would be @actual.should == @expected + 1), or we can return a boolean value as we’ve chosen to do. The other two methods allow us to define failure messages for failing should and should_not specs.

Our new class should behave in the same way as our simple matcher did, and the one spec we modified to deliberately fail should still fail but will now show the failure message we defined in the class.

$ rake spec
(in /Users/eifion/rails/apps_for_asciicasts/ep157)
..........F
1)
'ArticlesController should increment article position when creating next article' FAILED
expected 2 not to be one more than 1
./spec/models/article_spec.rb:12:

Our spec fails as expected and we’re now seeing the failure message we defined in the class’s failure_message_for_should_not method.

Macros

As well as matchers we can write RSpec macros to tidy up spec code. The specs for the ArticlesController cover the standard RESTful actions that the controller implements. Two of the specs describe actions that everyone can see: viewing a list of articles and seeing individual articles. The rest of the specs describe actions that are only available to admins (the actions that involve creating, editing and deleting articles). The whole file is a little long to display here, but you can see it on Ryan Bates’ Github pages under /spec/controllers. We’ll use macros to refactor some of this code.

The first part of the specs that we’re going to change is the code that creates a user before every admin spec is run.

describe "as admin" do
  before(:each) do
   user = User.new(:username => "Admin", :email => "admin@example.com", :password => "secret")
   user.admin = true
   user.save!
   session[:user_id] = user.id
  end
  # (rest of specs)
end

This code is used in several places which creates repetition in our specs. We could, instead, write a method called login_as_admin that creates a user and logs them and DRY up our spec code.

The method could go in the describe block, but then it would only be available to the specs for the ArticleController. To make it available to all of the controller specs we’ll create a new module and put the method there.

The new module will be created in a file called controller_macros.rb in the specs directory. We’ll call the module ControllerMacros and put the login_as_admin method there.

module ControllerMacros
  def login_as_admin
    user = User.new(:username => "Admin", :email => "admin@example.com", :password => "secret")
    user.admin = true
    user.save!
    session[:user_id] = user.id
  end
end

(What we’ve done here isn’t exactly a macro, but it has a similar effect and allows us to tidy up our spec code.)

To make the module accessible from our specs we need to modify the spec_helper.rb file again. Immediately below where we added the require line to add the custom matcher we’ll add a similar line to add the controller macros file.

require File.dirname(__FILE__) + "/controller_macros"

Likewise below where we included the CustomMatcher module we’ll include our ControllerMacros module.

config.include(ControllerMacros, :type => :controller)

The controller macros only need to be available to the controller specs, so we’ve added a type argument to restrict the specs that the module will be included in. With the module now available to all of the controller specs we can now use the login_as_admin method we created in any controller spec where we need to log in a user.

describe "as admin" do
  before(:each) do
	login_as_admin
  end
  # (rest of specs)
end

Another Problem

Our controller spec is now a little cleaner, but there’s another problem with it in that it’s missing a number of specs. In our application anyone should be able to view the index and show actions, but only logged-in users should see the actions that modify the articles. We have specs to test index and show as a guest and specs to test that a logged-in user can see the other actions. What’s missing are specs to test that guests cannot see the actions that require a login.

 describe "as guest" do
    it "index action should render index template" do
      get :index
      response.should render_template(:index)
    end
    it "show action should render show template" do
      get :show, :id => Article.first
      response.should render_template(:show)
    end
end

The guest specs don’t check that the admin pages are hidden.

These specs are important because without them there is no way of testing that the articles cannot be modified by anyone.

Our ArticlesController has a before filter in it that restricts access to some of the methods,

before_filter :admin_required, :except => [:index, :show]

but if we were to comment this filter out, all of our specs would still pass. The ArticlesController spec needs a new set of specs adding that check that only logged-in users can create and edit articles. Five new specs need to be added to the describe block above, one each for new, create, edit, update and destroy. Here is the one for new:

it "new action should require admin" do
  get :new
  response.should redirect_to(login_url)
  flash[:error].should == "Unauthorized Access"
end

The other four will all follow the same format, with only the string that describes the spec and the line that makes the request changing between each spec, so writing the specs will create a lot of repeated code. Instead of having to do that we could create a macro that would allow us to test the actions from a single method call.

it_should_require_admin_for_actions :new, :create, :edit, :update, :destroy

Creating The Macro

To create the macro we need to create a class method in the describe block.

def self.it_should_require_admin_for_actions(*actions)
  actions.each do |action|
    it "#{action} action should require admin" do
      get action, :id => 1
      response.should redirect_to(login_url)
      flash[:error].should == "Unauthorized Access"
    end
  end
end

The method loops through the actions we pass to it, and runs each one through an it block. The block then makes a GET request to that action, passing an id in case the action requires one, and checks that the response redirects to the login page and that the flash is showing the correct message.

This method will be useful in all of our controller specs so the next thing to do is to move it into our ControllerMacros module. Because of the way Ruby works, we can’t just move the method into the module as it won’t be included in the describe block that way. We’ll have to do a little more work to get the method working.

When a module is included in a class the module’s instance methods become available to instances of that class. The class methods, however, do not. To make them available we can make use of included. A module’s included method is called when the module is included into a class and it takes the class as an argument. We can use the included method, therefore, to extend the class with our module’s class methods.

First we’ll put the class method into a submodule that we’ll call ClassMethods. Then, in the included method we’ll extend the spec class with the class methods that are defined there (just the one for now). Now we’ll be able to use that method in any of our controller specs. Note that we’ve removed the self from before it_should_require_admin_for_actions as including it this way will automatically make it a class method.

module ControllerMacros
  def self.included(base)
    base.extend(ClassMethods)
  end
  module ClassMethods
    def it_should_require_admin_for_actions(*actions)
      actions.each do |action|
        it "#{action} action should require admin" do
          get action, :id => 1
          response.should redirect_to(login_url)
          flash[:error].should == "Unauthorized Access"
        end
      end
    end
  end
  def login_as_admin
    user = User.new(:username => "Admin", :email => "admin@example.com", :password => "secret")
    user.admin = true
    user.save!
    session[:user_id] = user.id
  end
end

We’ve used some advanced Ruby ideas in the module above. For the purposes of this example all you need to know is that the methods defined in the ClassMethods submodule are available in the describe blocks in the specs, while the methods outside the submodule are available in it blocks or before blocks.

By moving code into the module above we’ve managed to reduce a lot of the duplication in our specs. It’s worth remembering, however, that some duplication is desirable in specs. The specs are there to define your application’s behaviour and should do so clearly, even at the risk of duplication. You don’t want to hide your application’s behaviour behind the scenes.