Macro Tests

written by trotter on June 4th, 2008 @ 10:22 PM

I went to Josh Susser's talk at RailsConf last week, where he mentioned that no one really writes macro tests with rSpec. Naturally, I found this to be a true shame, since I think macro tests are a great way to clean up your code. What's a macro test you ask? Sit right there and I'll tell you.

A macro test is a test that defines other tests for you. It's a great way to reduce repetition in your test code, thereby making it easier to read. Take these three tests as an example:

describe 'GET /users/:user_id/posts/:post_id/comments' do
  it 'should be a success' do
    get :index, :user_id => 2, :post_id => 3
    response.should be_success
  end

  it 'should 404 without a user id' do
    get :index, :post_id => 3
    response.headers['Status'].to_i.should == 404
  end

  it 'should 404 without a post id' do
    get :index, :user_id => 2
    response.headers['Status'].to_i.should == 404
  end
end

Obviously there's a little bit of duplication in the 404's. Let's define macro tests to clean that up.

describe 'GET /users/:user_id/posts/:post_id/comments' do
  def self.should_404_without(param)
    # Since you're calling this method within the describe block, 
    # it is being called within the correct context.
    it "should 404 without #{param}" do
      get :index, paramz.merge(param.to_sym => nil)
      response.headers['Status'].to_i.should == 404
    end
  end

  should_404_without "user_id"
  should_404_without "post_id"

  # Normally I put this at the bottom, only here to keep it close to the above.
  # This method is called by the test the macro test defines to hand off the params.
  def paramz
    {:user_id => 2, :post_id => 3}
  end

  it 'should be a success' do
    get :index, paramz
    response.should be_success
  end
end

Hmm... this doesn't really look any cleaner. The problem is that we did this for only one action. If we generalize should_404_without more, then we can put it into its own shared example set that we can include in any describe block. Let's look at that now.

shared_examples_for "controllers" do
  def self.should_normally_succeed
    it 'should be a success' do
      get @action, paramz
      response.should be_success
    end
  end
  
  def self.should_404_without(param)
    it "should 404 without #{param}" do
      get @action, paramz.merge(param.to_sym => nil)
      response.headers['Status'].to_i.should == 404
    end
  end
end

describe 'GET /users/:user_id/posts/:post_id/comments' do
  it_should_behave_like "controllers"
  
  before(:each) { @action = :index }

  should_normally_succeed
  should_404_without "user_id"
  should_404_without "post_id"

  def paramz
    {:user_id => 2, :post_id => 3}
  end
end

Oh snap! That describe block is a lot cleaner, and we can tuck the shared examples into spec_helper.rb to really clean things up. Using macro tests, you'll find it's very easy to create a lot of REST controllers very quickly. In fact, I plan on open sourcing something to help with that very soon...

Update: See the comments for David's advice on pulling this out of shared examples and into a module that you can then add to rSpec. This cleans up the describe blocks even more. Mega win!

Comments

  • David Chelimsky on 07 Jun 13:04

    Hey Trotter - this is good stuff. The one thing I might do differently is create a module and include it in the config in spec_helper.rb like this:
    module ControllerSpecMacros #or whatever
      module Macros #or whatever
        def should_normally_succeed
          it 'should be a success' do
            get @action, paramz
            response.should be_success
          end
        end
      
        def should_404_without(param)
          it "should 404 without #{param}" do
            get @action, paramz.merge(param.to_sym => nil)
            response.headers['Status'].to_i.should == 404
          end
        end
      end
      
      class << self
        def included(mod)
          mod.extend Macros
        end
      end
    end
    
    Spec::Runner.configure do |config|
      # ...
      config.include(ControllerSpecMacros, :type => :controller)
      # ...
    end
    
    That way you don't have to say #it_should_behave_like in every controller example group. HTH. Cheers, David
  • Trotter Cashion on 09 Jun 12:06

    David, that's definitely a lot nicer. I'll be doing that in the future.

Comments are closed

Options:

Size

Colors