How Would You Test This?
(Sat Sep 08, 2007) [/Rails] #
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.
Now it's your turn to blog up a solution. Which tools and techniques are working for you? Send me links and I'll write a follow-up post summarizing the results. As well, I'm looking forward to seeing how Jim and Joe solve this in their Test-Driven Development with Rails Studio next month. I'd enjoy the opportunity to chat with you there.
