Tidying up your RSpecs – 1

This is a cross post from our company blog.

Almost everyone in Ruby world agrees that RSpecs are a great way to test code in both Ruby and Rails projects.

Today, I want to share some of the tips which I had learnt and used in my RSpecs which has really help to tidy up the RSpec code and output.

Explicit Subjects & Contexts

Consider a RSpec for a Rails Controller. Assume that controller actions require a user to authenticate himself before performing any of the actions. In order to test this aspect we can write RSpec like this

describe PlacesController do
  describe "access control" do
    describe "for non-signed in users" do
      it "should redirect to sign in page for delete :destroy" do
        delete :destroy, :id => "1234"
        response.should redirect_to(new_user_session_path)
      end
      it "should redirect to sign in page for get :index" do
        get :index
        response.should redirect_to(new_user_session_path)
      end
      it "should redirect to sign in page for get :show" do
        get :show, :id => "123"
        response.should redirect_to(new_user_session_path)
      end
    end
  end

The above specs can be repeated for each and every action in the controller. Now when I run the above spec the output will look something like this.

for non-signed in users
  should redirect to sign in page for delete :destroy
  should redirect to sign in page for get :index
  should redirect to sign in page for get :show

The output doesn’t read really that great. One of the good guideline which I believe for writing good RSpec is that the output should be really readable like a prose and easily convey the behavior or intent of the code it tests. So it’s time to tweak the code and make the output more readable. Also if you look the code has lot of repeatition of response.should redirect_to all over the place. So let us clean up that with explicit subject and context.

describe PlacesController do
  describe "access control" do
    context "for non-signed in users" do

      subject { response }

      context "delete :destroy" do
        before { delete :destroy, :id => "1234" }
        it { should redirect_to(new_user_session_path) }
      end

      context "get :index" do
        before { get :index }
        it { should redirect_to(new_user_session_path) }
      end

      context "get :show" do
        before { get :show, :id => "123" }
        it { should redirect_to(new_user_session_path) }
      end
    end
  end
end

Notice how I have used the subject {} to specify an explicit subject. The implicit subject for any RSpec is the class which is used with the describe at top. In the above case it would be PlacesController. By specifying an explicit subject we are declaring the object which need to evaluated for subsequent tests. In this case it has been set to the response object. The usage of context really makes the code much clean and readable. Basically it sets the context for each test case while running. Now if we run the RSpec the output looks much cleaner like this.


for non-signed in users
  delete :destroy
    should redirect to "/users/sign_in"
  get :index
    should redirect to "/users/sign_in"
  get :show
    should redirect to "/users/sign_in"

Here is another example of an explicit subject below. This is an RSpec for User Role assignments with CanCan.


describe Ability do
  context "Admin Role" do
    before(:each) do
      @admin = Factory(:admin)
      @ability = Ability.new(@admin)
    end

    subject { @ability }
    it { should be_able_to(:manage,User) }
    it { should be_able_to(:manage, Restuarant)}
    it { should be_able_to(:manage,Place) }
  end
end

The code really is looking succinct and clean with the usage of subject {} . Without the subject it would have been a drudgery to write the same piece with describe…it..end blocks.

Shared Examples

Now let us turn our attention to the technique of Shared Examples which keeps our RSpec code DRY. Here is an example which tests the Links appearing in a footer of the page based on the User Role.

shared_examples "footer" do |links|
  links.each do |link|
    it "with #{link}" do
      within("div#footer") do
        page.should have_xpath("//nav/ul/li/a[contains(text(),'#{link}')]")
      end
    end
  end
end

describe "Footer" do
  extend LinkHelpers

   context "not signed in user" do
    before { visit search_home_path }
    it_behaves_like "a footer", normal_links
   end

   context "Admin" do
     before(:each) do
      @admin = Factory(:admin)
      sign_in_user(@admin.email,"password")
     end
     it_behaves_like "a footer", admin_links
   end
end

The code is pretty self-explanatory. For a noumal user I want a bunch of links to appear in the footer. For an admin user I want another set of links to appear. In the code above normal_links method gives the array of links for normal user and admin_links gives the array of links for admin user. [Note normal_links and admin_links are methods defined in a module LinkHelpers which I am extending below describe]

Coming to the DRY part. The code for doing the actual testing is put under a shared_example “footer” and it is invoked with it_behaves_like call. Also note we can pass zero or any number of arguments to shared_example. In the example above it accepts a single argument of Array type.

Let us see the output of the above code:

Footer
   not signed in user
     behaves like a footer
       with About Us
       with Feedback
       with Disclaimer
   Admin
     behaves like a footer
       with About Us
       with Feedback
       with Disclaimer
       with Manage Restaurant
       with Manage Place

Even though the behavior and output is correct, the above results and code doesn’t read proper. Probably we shouldn’t call display of footer as a behavior. RSpec to rescue again! We can customize what appears in the output (in this case “behaves like a”) as well as rename the function it_behaves_like to something which is more appropriate for the context.

So for diplay of a footer it may be good to have a function like it_has_a “footer” instead of it_behaves_like “a footer”. Now let us see how this done:

RSpec.configure do |c|
   c.alias_it_should_behave_like_to :it_has_a, 'has a'
end

shared_examples "footer" do |links|
   links.each do |link|
     it "with #{link}" do
       within("div#footer") do
         page.should have_xpath("//nav/ul/li/a[contains(text(),'#{link}')]")
       end
     end
   end
end

describe "Footer" do
   extend LinkHelpers

   context "not signed in user" do
     before { visit search_home_path }
     it_has_a "footer", normal_links
   end

   context "Admin" do
     before(:each) do
       @admin = Factory(:admin)
       sign_in_user(@admin.email,"password")
     end
     it_has_a "footer", admin_links
  end
end

In the above code RSpec.configure block actually aliases the it_behaves_like function it_has_a and specifies that “has a” should be the output instead of “behavies like a” while the specs are run. Let us check the output now:


Footer
   not signed in user
     has a footer
       with About Us
       with Feedback
       with Disclaimer
   Admin
     has a footer
       with About Us
       with Feedback
       with Disclaimer
       with Manage Restaurant
       with Manage Place

To Conclude

  • Use context with describe and make the test more readable instead of just getting stuck with describe..describe…it kind of format.
  • Give a before {} block for any context to set any pre-conditions before running the context.
  • Wherever possible declare an explicit subject and reduce the cruft in the specs.
  • DRY up your RSpec with shared_examples.
  • Customize the method name and output by aliasing in RSpec.configure.

Hope this post was helpful, do share your thoughts if any!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s