\$\begingroup\$

I intend this to be a general question on writing an effective set of test cases for a controller action.

I include the following ingredients:

Ruby on Rails

RSpec: A testing framework. I considered doing a vanilla Rails question, but I personally use RSpec, and I feel many other folks do too. The principles that come out of this discussion should be portable to other testing frameworks, though.

A testing framework. I considered doing a vanilla Rails question, but I personally use RSpec, and I feel many other folks do too. The principles that come out of this discussion should be portable to other testing frameworks, though. Will Paginate: I include this to provide an example of code whose implementation is blackboxed to the controller. This is an extreme example of just using the model methods like @programs = Program.all . I chose to go this route to incorporate an additional factor for discussion, and to demonstrate that the same principles apply whether using external application code (e.g., model code) or an external plugin.

There seems to be a lack, given my humble Google Fu, of style guides for RSpec testing at this level, so it is my hope that, on top of me improving my code, this can become a useful guide for my fellow travelers.

For example purposes, let's say I currently have in my controller the following:

class ProgramssController < ApplicationController def index @programs = Program.paginate :page => params[:page], :per_page => params[:per_page] || 30 end end

Sidebar: For those unfamiliar with will_paginate , it tacks onto a ActiveRecord Relation ( all , first , count , to_a are other examples of such methods) and delivers a paginated result set of class WillPaginate::Collection , which basically behaves like an array with a few helpful member methods.

What are the effective tests I should run in this situation? Using RSpec, this is what I've conceived at the moment:

describe ProgramsController do def mock_program(stubs={}) @mock_program ||= mock_unique_program(stubs) end def mock_unique_program(stubs={}) mock_model(Program).as_null_object.tap do |program| program.stub(stubs) unless stubs.empty? end end describe "GET index" do it "assigns @programs" do Program.stub(:paginate) { [mock_program] } get :index response.should be_success assigns(:programs).should == [mock_program] end it "defaults to showing 30 results per page" do Program.should_receive(:paginate).with(:page => nil, :per_page => 30) do [mock_program] end get :index response.should be_success assigns(:programs).should == [mock_program] end it "passes on the page number to will_paginate" do Program.should_receive(:paginate).with(:page => '3', :per_page => 30) do [mock_program] end get :index, 'page' => '3' response.should be_success assigns(:programs).should == [mock_program] end it "passes on the per_page to will_paginate" do Program.should_receive(:paginate).with(:page => nil, :per_page => '15') do [mock_program] end get :index, 'per_page' => '15' response.should be_success assigns(:programs).should == [mock_program] end end end

I wrote this with the following principles in mind:

Don't test non-controller code: I don't delve into the actual workings of will_paginate at all and abstract away from its results.

I don't delve into the actual workings of at all and abstract away from its results. Test all controller code: The controller does four things: assigns @programs , passes on the page parameter to the model, passes on the per_page parameter to the model, and defaults the per_page parameter to 30, and nothing else. Each of these things are tested.

The controller does four things: assigns , passes on the parameter to the model, passes on the parameter to the model, and defaults the parameter to 30, and nothing else. Each of these things are tested. No false positives: If you take away the method body of index , all of the test will fail.

If you take away the method body of , all of the test will fail. Mock when possible: There are no database accesses (other ApplicationController logic notwithstanding)

My concerns, and hence the reason I post it here: