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.
- 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 classWillPaginate::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. - Test all controller code: The controller does four things: assigns
@programs
, passes on thepage
parameter to the model, passes on theper_page
parameter to the model, and defaults theper_page
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. - Mock when possible: There are no database accesses (other ApplicationController logic notwithstanding)
My concerns, and hence the reason I post it here:
- Am I being too pedantic? I understand that TDD is rooted in rigorous testing. Indeed, if this controller acts as a part of a wider application, and say, it stopped passing on
page
, the resulting behaviour would be undesirable. Nevertheless, is it appropriate to test such elementary code with such rigor? - Am I testing things I shouldn't be? Am I not testing things I should? It think I've done an okay job at this, if anything veering on the side of testing too much.
- Are the
will_paginate
mocks appropriate? You see, for instance, with the final three tests, I return an array containing only one program, whereaswill_paginate
is quite liable to return more. While actually delving this behaviour by adding, say 31 records, may (?) violate modular code and testing, I am a bit uneasy returning an unrealistic or restrictive mock result. - Can the test code be simplified? This seems quite long to test one line of controller code. While I fully appreciate the awesomeness of TDD, this seems like it would bog me down. While writing these test cases were not too intensive on the brain and basically amounted to a fun vim drill, I feel that, all things considered, writing this set of tests may cost more time than it saves, even in the long run.
- Is this a good set of tests? A general question.