Ruby on Rails: Using RSpec Macros and Metadata

I have to hand it to you: you're a great Rails developer! I just read through the code you've been writing for that new project and you're doing it right. You've got fast, isolated tests with RSpec, integration tests in well-written Cucumber scenarios, and have you lost weight or are your controllers skinnier? Just one nit-pick, though—where are your controller specs?

If you follow best practices, I understand that it can seem unnecessary to test your controllers: they're so small, they are all the same, and it's pretty hard to get it wrong. Indeed, it would probably take longer to write the tests than it would to write the controller itself and in any event you already have integration tests that are covering most of the controller code.

Let me tell you why I still think you should write controller tests:

  • They are very easy to write
  • They are mostly the same
  • Because they are mostly the same, we can refactor and make them even easier to write

We all refactor our code, but it's just as important to refactor our tests. The goal of refactoring is to facilitate reuse and increase clarity. Refactoring let's us develop new abstractions upon which we can build complex logic. Our controllers are so lean and mean because they are benefitting from a very convenient abstraction: RESTful web services.

Chances are most of the controllers in your app have a lot of shared behaviors. They find, create, and update your models, set flash messages, redirect or render templates, and set status codes. Let's take advantage of the shared behaviors to refactor our tests.

Put the following file into spec/support/controller_helpers.rb

module ControllerHelpers
  extend ActiveSupport::Concern

  module ClassMethods
    def self.define_action(action)
      define_method action do |*args, &block|
        options = args.extract_options!
        options[:http_method] = action
        options[:controller_method] = args[0]
        args[0] = [action.to_s.upcase, "#" + options[:controller_method].to_s].join(" ")
        args << options
        context(*args, &block)
      end
    end

    define_action :get
    define_action :post
    define_action :put
    define_action :delete
    define_action :head
    define_action :options

    def it_should_flash(type, message)
      it "should set the flash" do
        do_request
        flash[type].should eq(message)
      end
    end

    def it_should_redirect_to(&block)
      it "should redirect" do
        url = instance_eval(&block)
        do_request
        response.should redirect_to(url)
      end
    end

    def it_should_render_template(template)
      it "should render the #{template} template" do
        do_request
        response.should render_template(template)
      end
    end
  end

  def do_request
    params = if respond_to?(:params) then send(:params) else nil end # of story
    send(example.metadata[:http_method], example.metadata[:controller_method], params)
  end
end

RSpec.configure do |config|
  config.include ControllerHelpers, :type => :controller
end

Now we can write our controller specs easily. For example:

require "spec_helper"

describe FoosController do
  get :new do
    it_should_render_template :new
  end

  post :create do
    let(:params) { "foo" => { "bar" => "baz" } }
    let(:foo) { stub }

    before { Foo.stub!(:new).with("bar" => "baz").and_return(foo) }

    context "successful create" do
      before { foo.stub!(:save).and_return(true) }

      it_should_set_flash :success, "Created foo."
      it_should_redirect_to { foos_url }
    end

    context "failed create" do
      before { foo.stub!(:save).and_return(false) }

      it_should_render_template :new
    end
  end
end

Enjoy!

Check out our other articles on ruby on rails tips and best practices to keep your project optimized:

Application Development Process Tip: Reverse Ticket Priorities on Friday

9 Application Development Productivity Hacks

From Idea to Product: The Application Development Process