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
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.