Testing Controllers with rspec
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


