Macro Tests
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
-
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) # ... endThat way you don't have to say #it_should_behave_like in every controller example group. HTH. Cheers, David -
David, that's definitely a lot nicer. I'll be doing that in the future.