Testing Controllers with rspec

In How Would You Test This?, Mike Clark asks for input on how to test a straightforward controller. Here’s how we do it.

Firstly, we use rspec exclusively, so I’m only going to tackle that method of testing. Our goal with controller tests is to create very simple, easy to read tests that cover the entire controller. We believe strongly in one assertion per test, so we will have a larger number of small tests. Our typical pattern is to stub everything at the beginning, and then mock as necessary to validate behavior.

So here’s the first cut:

require File.dirname(__FILE__) + '/../spec_helper'

describe MenuItemsController, "new with a valid menu item" do
  before(:each) do
    MenuItem.stub!(:new).and_return(@menu_item = mock_model(MenuItem, :save => true))
  end
  
  def do_create
    post :create, :menu_item => {:name => "value"}
  end
  
  it "should create the menu item" do
    MenuItem.should_receive(:new).with("name" => "value").and_return(@menu_item)
    do_create
  end
  
  it "should save the menu item" do
    @menu_item.should_receive(:save).and_return(true)
    do_create
  end
  
  it "should be redirect" do
    do_create
    response.should be_redirect
  end
  
  it "should assign menu_item" do
    do_create
    assigns(:menu_item).should == @menu_item
  end
  
  it "should redirect to the index path" do
    do_create
    response.should redirect_to(menu_items_url)
  end
end

describe MenuItemsController, "new with an invalid menu item" do
  before(:each) do
    MenuItem.stub!(:new).and_return(@menu_item = mock_model(MenuItem, :save => false))
  end
  def do_create
    post :create, :menu_item => {:name => "value"}
  end
  
  it "should create the menu item" do
    MenuItem.should_receive(:new).with("name" => "value").and_return(@menu_item)
    do_create
  end
  
  it "should save the menu item" do
    @menu_item.should_receive(:save).and_return(false)
    do_create
  end
  
  it "should be success" do
    do_create
    response.should be_success
  end
  
  it "should assign menu_item" do
    do_create
    assigns(:menu_item).should == @menu_item
  end
  
  it "should re-render the new form" do
    do_create
    response.should render_template("new")
  end
end

Yes, that’s a lot of code for testing a simple method, but it’s also really easy to read. Each test does one thing only. That means when I accidently re-name the assigned variable from @menu_item to @emu_item, we will only have one failure for each case. It also gives us a very nice specification document that I’ve included below.

You’ll also notice that we went from having two tests to having a large number of tests in two different contexts. We use a different context for each different behavior we expect.

That means there will be some duplication between the contexts. I wouldn’t refactor the controller spec to eliminate duplication. I’m okay with having the same tests in the two contexts. If I saw that pattern quite a few times, I might break it out into a shared behavior which I could include in both, but I probably wouldn’t. In my mind, duplication in tests is a strong sign. If I’m seeing a lot of duplication, it probably means I’m testing 4 or 5 different cases in my controllers. If my controllers have that much logic that needs testing, that is a sign of a bigger problem that duplicated test code. That typically means my controllers are getting too fat.

Finally, we create a do_create method that actually calls post :create .... This isn’t strictly necessary, but just looks a little better to me.

So to summarize, we:

  • Stub everything in the setup and mock to check validation
  • Have one assertion per test
  • Use a different context for different behavior
  • Use a method (do_create) to remove duplication of the action from the tests

So what would I do differently? I don’t like checking return values, so I would change the controller to look like:

  def create
    @menu_item = MenuItem.new(params[:menu_item])
    @menu_item.save!
    flash[:notice] = 'MenuItem was successfully created.'
    redirect_to menu_items_url
  rescue ActiveRecord::RecordInvalid
    render :action => :new
  end

Here’s the rspec specdoc output from our tests:

RSpec Results

 

 

MenuItemsController new with a valid menu item
should create the menu item
should save the menu item
should be redirect
should assign menu_item
should redirect to the index path
MenuItemsController new with an invalid menu item
should create the menu item
should save the menu item
should be success
should assign menu_item
should re-render the new form