Writing Macros in RSpec

Thoughtbot put up a good post about creating "macro"s with Shoulda . Shoulda has some very nice built-in macros which help a lot in keeping tests DRY without sacrificing the documentation aspect of tests. (In fact I think these improve the readability of most tests because it removes some of the noise.) As Tammer Saleh said in the post these "macros" are nothing more than normal class methods, and so you can have the same sort of macros in any testing framework. I have been creating and using my own custom macros in RSpec for some time now. RSpec’s awesome DSL adds a little more complexity when creating such macros, but it is still easy to do once you understand some of RSpec’s DSL internals. In this article I explain RSpec’s DSL enough to provide insight on how to write macros. I will then show an example of writing a macro and illustrate how to extract it for reuse.

I should preface this explanation by saying that RSpec’s built in Shared Behaviour capabilities are very powerful and are usually the appropriate solution when abstracting common behaviour. Pat Maddox recently outlined some best practices of using shared behaviours in a refactoring . I generally use macros when I want to pass in an argument so that I can either a) use them multiple times in a given describe context, or b) customize the example and/or the description for specdoc output.

Macro Example: it_should_assign

A useful macro I use in my Rails controller specs is "it_should_assign". For example, say we have this simple show action on a nested resource controller:

  
class UserPhotosController
  def show
    @user = User.find(params['user_id'])
    @photo = @user.photos.find(params['id'])
    @page_title = "Best photo evar."
  end
end

And the specs dealing with assignment:

  
describe UserPhotosController do

  describe "GET 'users/1/photos/2'" do

    before(:each) do
      User.stub!(:find).and_return( @user = mock_user )
      @user.stub_association!(:photos, :find => (@users_photo = mock_model(Photo)) )
    end

    def do_get
      get :show, :user_id => @user.id, :id => @users_photo.id
    end

    ...

    it "should assign the user to the view" do
      do_get
      assigns[:user].should == @user
    end

    it "should assign the photo to the view" do
      do_get
      assigns[:photo].should == @users_photo
    end

    it "should assign the page title to the view" do
      do_get
      assigns[:page_title].should == "Best photo evar."
    end

  end

end
  

As you can see, the three specs are very similar and have some duplication between them. I should point out that duplication in tests is expected and is usually not a bad thing since documentation and readability are primary goals of having a test/spec suite. When extracting duplication from your specs you need to be very careful that these two aspects of the specs are not diminished. A lot of this will depend on the situation and the skill level of your team. In this example I would argue that this is such a common pattern that an abstraction wouldn’t hurt readability and in fact would help reduce some of the implementation noise (such as ‘do_get’.)

How do we want the "it_should_assign" macro to work? This example illustrates the different ways I use it in my specs:

  
describe UserPhotosController do

  describe "GET 'users/1/photos/2'" do

    before(:each) do
      User.stub!(:find).and_return( @user = mock_user)
      @user.stub_association!(:photos, :find => (@users_photo = mock_model(Photo, :caption => "Best photo evar.")) )
    end

    def do_get
      get :show, :user_id => @user.id, :id => @users_photo.id
    end

    it_should_assign :user # Here, the macro assumes assigns[:user].should == @user
    it_should_assign :photo, "@users_photo" # Have flexibility when the name differs in the spec
    it_should_assign :page_title, "Best photo evar." # Ability to check literals

  end

end
  

Dissecting part of RSpec’s DSL

Before I show the implementation, it is important to realize what the "describe" keyword really does. The "describe" keyword in rspec is actually a factory method (that in turn delegates to a factory object) that creates ExampleGroup sub-classes. In rspec source terminology your "describe" blocks are example groups and your "it" blocks are the examples. So, you can think of "describe" as being a sort of wrapper for "class." To make this point more clear, we can forgo the use of the describe keyword and subclass the ExampleGroup class ourselves:

  
class UserPhotosControllerSpec < Spec::Rails::Example::ControllerExampleGroup
  describe UserPhotosController, "GET 'users/1/photos/2'"

    before(:each) do
      User.stub!(:find).and_return( @user = mock_user)
      @user.stub_association!(:photos, :find => (@users_photo = mock_model(Photo, :caption => "Best photo evar.")) )
    end

    def do_get
      get :show, :user_id => @user.id, :id => @users_photo.id
    end

    it_should_assign :user
    it_should_assign :photo, "@users_photo"
    it_should_assign :page_title, "Best photo evar."

  end

end

  

Inline the macro implementation into the current example group

Alright, so "describe" just subclasses the correct example group class for us and the block that gets passed in becomes our class definition. With that knowledge we can create our macro as a regular class method inside our example group:

  
describe UserPhotosController do

  describe "GET 'users/1/photos/2'" do

    before(:each) do
      User.stub!(:find).and_return( @user = mock_user)
      @user.stub_association!(:photos, :find => (@users_photo = mock_model(Photo)) )
    end

    def do_get
      get :show, :user_id => @user.id, :id => @users_photo.id
    end

    def self.it_should_assign(variable_name, value=nil)
      it "should assign #{variable_name} to the view" do
        value ||= instance_variable_get("@#{variable_name}")
        if value.kind_of?(String) && value.starts_with?("@")
          value = instance_variable_get(value)
        end
        do_get
        assigns[variable_name].should == value
      end
    end

    it_should_assign :user
    it_should_assign :photo, "@users_photo"
    it_should_assign :page_title, "Best photo evar."

  end

end
  

Pretty simple, huh? At this point the macro is only available to that example group. There are several ways you can extract it so that it can be used elsewhere. Before we extract the macro we need to address the problem with the "do_get" call.

In my controller specs I follow the convention of defining a "do_get", "do_put", etc. based on the HTTP verb the action responds to. In order for the assigns macro to work across specs for different actions it needs to be able to call the correct do_verb method. We can accomplish this by defining a "do_action" method which calls the correct method:

  
describe UserPhotosController do

  describe "GET 'users/1/photos/2'" do

    ...

    def do_action
      verb = [:get, :post, :put, :delete].find{|verb| respond_to? :"do_#{verb}"}
      raise "No do_get, do_post_ do_put, or do_delete has been defined!" unless verb
      send("do_#{verb}")
    end

    def do_get
      get :show, :user_id => @user.id, :id => @users_photo.id
    end

    def self.it_should_assign(variable_name, value=nil)
      it "should assign #{variable_name} to the view" do
        ...
        do_action
        assigns[variable_name].should == value
      end
    end
    ...

  end

end
  

Extracting the macro

With the do_verb calls abstracted into the do_action method we can now extract the macro. The most flexible option is to turn it into a module. The following is a standard ruby module taking advantage of the Module#included hook so both instance and class methods are mixed in:

  
module AssignMacro
  module ExampleMethods
    def do_action
      verb = [:get, :post, :put, :delete].find{|verb| respond_to? :"do_#{verb}"}
      raise "No do_get, do_post_ do_put, or do_delete has been defined!" unless verb
      send("do_#{verb}")
    end
  end

  module ExampleGroupMethods
    def it_should_assign(variable_name, value=nil)
      it "should assign #{variable_name} to the view" do
        value ||= instance_variable_get("@#{variable_name}")
        if value.kind_of?(String) && value.starts_with?("@")
          value = instance_variable_get(value)
        end
        do_action
        assigns[variable_name].should == value
      end
    end
  end

  def self.included(receiver)
    receiver.extend         ExampleGroupMethods
    receiver.send :include, ExampleMethods
  end
end

# Now, we can just include it wherever we need it...
describe UserPhotosController do

  describe "GET 'users/1/photos/2'" do
    include AssignMacro
    ...
  end

end
  

Extracting the macros out into modules allows you to mix them only into example groups that you want them to be in. In the case of this macro it would be nice to have it available for all controller specs. This is possible by monkey patching the ControllerExampleGroup in your spec_helper.rb:

  
module Spec::Rails::Example

  class ControllerExampleGroup
    include AssignMacro
  end

end
  


UPDATE
Please see David’s comment at the bottom of the post. It turns out that RSpec’s API provides a safer way to mix in modules into certain example groups. Monkey patching can be brittle and subject to breaks if RSpec’s underlying structure/implementation changes.

For controller macros I usually don’t take the extra step of extracting it into a standalone module. Instead, I just monkey patch my class and instance methods directly into ControllerExampleGroup. If you have a macro that you want available for all of your example groups rspec already has a place where you can put them:

  
    module Spec::Example

      module ExampleGroupMethods
        # place example group methods (class methods) here
      end

      module ExampleMethods
        # place your example helper methods (instance methods) like do_action here
      end

  end

  

Summary

Macros are useful when extracting granular facets of behaviour which you want to customize by passing in arguments. They are similar to shared behaviours but live at a different level (the class level) which allows for the additional flexibility. Shared behaviours are usually a better fit when a refactoring causes you to consolidate behaviour in a module or a class higher up in the inheritance chain. Macros tend to evolve out of common usage patterns within your specs and should be created when you want to dynamically create these slightly different specs. Documentation and readability are paramount when using both methods. If a macro saves you a couple of lines of duplicated code, but ends up hurting those two aspects of the spec, don’t use it!

I hope this article is helpful to people new to RSpec and to those who haven’t yet discovered this pattern. If anything is unclear just ask. :) Several people have asked me to release some of my macros and other rspec extensions that I use in all of my rails and merb projects. When I get the time I will clean them up and put them onto my github account . Until then, happy spec’ing!


About this entry