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_aare 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_paginateat all and abstract away from its results.
- Test all controller code: The controller does four things: assigns @programs, passes on thepageparameter to the model, passes on theper_pageparameter to the model, and defaults theper_pageparameter 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_paginatemocks appropriate? You see, for instance, with the final three tests, I return an array containing only one program, whereaswill_paginateis 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.