It's not a trick question, and there is no right or wrong answer. The state of the art of Rails testing has evolved since the early days. Keeping up has proven both rewarding and at the same time a bit of a struggle. I'm continually refining the way I write tests to help break writing the code down into small, safe steps and defer decisions with confidence. And I'm generally curious to see how you're testing controllers these days.

Why controllers? Simply because they feel more challenging to test than models. And folks who are perfectly fine with test-driven development at the model level are often overcome with reluctance and frustration when they start coding controllers. The hesitation has an upside: It prompts one to move business logic into models, where the logic belongs and where it's easy to test.

However, the frustration has a downside. Having fattened up the models, it's easy to then throw in the towel when it comes to testing controllers. But given that we're writing web applications—and controllers are central to how the world accesses our models—it seems fairly important to test controllers. That means being able to easily flesh out what can happen at a specific place: Given a particular state, if you poke a controller action in a particular way, what happens to the state (model objects, the session, visible display, etc.)?

Assume, for example, that you have the following simple create action in a controller, or assume you want to use TDD to force the writing of a similar create action:

class MenuItemsController < ApplicationController
  
  def create
    @menu_item = MenuItem.new(params[:menu_item])
    if @menu_item.save
      flash[:notice] = 'MenuItem was successfully created.'
      redirect_to menu_items_url
    else
      render :action => :new
    end
  end
  
  def new
    @menu_item = MenuItem.new
  end
  
end

Nothing exciting going on here. It's basically the same create action you've been writing since Rails 0.10.0. (I've removed the use of respond_to in this example just to keep it as simple as possible.) Testing this action isn't especially difficult, but as an example it allows us to focus on style. And in this brave new REST world where most controllers will have a create action similar to the one above, style matters. If the mechanics of writing a test get in the way of describing behavior we aim to implement, they'll we'll hate writing tests in exactly the way programmers are supposed to hate testing.

So, how would you go about validating the behavior of the create action? I'm hoping that by soliciting solutions we can all learn something. To help get things started, I offer the following styles that I've observed across a number of projects.

The Classic Cheeseburger

Your basic, get 'er done functional test topped with an assert_difference custom assertion (the cheese) that checks whether a new menu item was created in the database.

class MenuItemsControllerTest < Test::Unit::TestCase
  
  def setup
    @controller = MenuItemsController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new
  end

  def test_create_with_valid_menu_item
    assert_difference(MenuItem, :count, 1) do
      post :create, :menu_item => {:name => 'Classic', 
                                   :price => 4.99}
    end
      
    assert_not_nil assigns(:menu_item)
    assert_not_nil flash[:notice]
    assert_redirected_to menu_items_url
  end
  
  def test_create_with_invalid_menu_item
    assert_difference(MenuItem, :count, 0) do
      post :create, :menu_item => { }
    end
  
    assert_not_nil assigns(:menu_item)
    assert_nil flash[:notice]
    assert_response :success
    assert_template 'new'
  end

end

Hold The Onions

This is the classic cheeseburger, marinated with FlexMock to remove the bitter aftertaste of the database. Tastes best with a healthy side dish of unit tests that check model validations, so the functional test doesn't have to.

require 'flexmock/test_unit'    

class MenuItemsControllerTest < Test::Unit::TestCase
  
  def setup
    @controller = MenuItemsController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new
    
    @attributes = { 'name' => 'No Onions', 'price' => 4.99 }
    @menu_item = flexmock(MenuItem.new(@attributes))
    flexmock(MenuItem).should_receive(:new).with(@attributes).once.
      and_return(@menu_item)
  end
  
  def test_create_with_valid_menu_item
    @menu_item.should_receive(:save).with().once.and_return(true)

    post :create, :menu_item => @attributes

    assert_equal @menu_item, assigns(:menu_item)
    assert_not_nil flash[:notice]
    assert_redirected_to menu_items_url
  end

  def test_create_with_invalid_menu_item
    @menu_item.should_receive(:save).with().once.and_return(false)

    post :create, :menu_item => @attributes

    assert_equal @menu_item, assigns(:menu_item)
    assert_nil flash[:notice]
    assert_response :success
    assert_template 'new'
  end

end

The Daily Spec-ial

A light, sugary topping of test/spec on an old favorite, with a distinct flavor of BDD.

require 'test/spec'   
require 'test/spec/rails' 
require 'flexmock/test_unit'    

context 'Creating a new menu item' do
  
  use_controller MenuItemsController

  setup do
    @attributes = { 'name' => 'Daily Special', 'price' => 4.99 }
    @menu_item = flexmock(MenuItem.new(@attributes))
    flexmock(MenuItem).should_receive(:new).with(@attributes).once.
      and_return(@menu_item)
  end
  
  specify 'should redirect to index with a notice on successful save' do
    @menu_item.should_receive(:save).with().once.and_return(true)

    post :create, :menu_item => @attributes

    assigns(:menu_item).should.equal @menu_item
    flash[:notice].should.not.be.nil    
    should.redirect_to menu_items_url
  end
  
  specify 'should re-render new template on failed save' do
    @menu_item.should_receive(:save).with().once.and_return(false)

    post :create, :menu_item => @attributes

    assigns(:menu_item).should.equal @menu_item
    flash[:notice].should.be.nil
    status.should.be :success
    template.should.be 'new'
  end
  
end

The Whole Enchilada

This ain't no burger. It's an entirely different kind of (test-lingo-free) taste, smothered in a spicy RSpec syntax and served with a side of refried mocking.

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

describe MenuItemsController, 'Creating a new menu item' do

  before do
    @attributes = {'name' => "Enchilada", 'price' => 4.99}
    @menu_item = mock_model(MenuItem)
    MenuItem.should_receive(:new).with(@attributes).once.
      and_return(@menu_item)
  end
  
  it 'should redirect to index with a notice on successful save' do
    @menu_item.should_receive(:save).with().once.and_return(true)
    
    post :create, :menu_item => @attributes
    
    assigns[:menu_item].should be(@menu_item)
    flash[:notice].should_not be(nil)    
    response.should redirect_to(menu_items_url)
  end

  it 'should re-render new template on failed save' do
    @menu_item.should_receive(:save).with().once.and_return(false)

    post :create, :menu_item => @attributes

    assigns[:menu_item].should be(@menu_item)
    flash[:notice].should be(nil)    
    response.should be_success
    response.should render_template('new')
  end
  
end

This menu of controller testing styles is far from comprehensive, and it's not intended to be a subjective comparison. I specifically tried to introduce subtle variations along the way, while preserving the initial goal of testing the create action. Each style has its pros and cons. Perhaps just seeing a few different styles will trigger new ideas.