Controllers

Basic Controller helpers

Just like the ActiveRecord macros, Shoulda gives you a set of macros to make testing your controllers as clear as possible. All of these methods are in the Shoulda rDocs, but here’s a quick example:


class UsersControllerTest < Test::Unit::TestCase
  context "on GET to :show" do
    setup { get :show, :id => 1 }

    should_assign_to :user
    should_respond_with :success
    should_render_template :show
    should_not_set_the_flash

    should "do something else really cool" do
      assert_equal 1, assigns(:user).id
    end
  end

  context "on POST to :create" do
    setup { post :create, :user => {:name => 'Ronald', :party => 'Repukeulan' } }
    should_assign_to :user
    should_redirect_to "user_url(@user)" 
    should_set_the_flash_to(/created/i)
  end
end

Should be RESTful

Each of these should_xxx macros will create a separate test method. While these can really dry up your functional tests, the should_be_restful macro can generate entire swaths of tests—assuming your controller follows the basic RESTful design pattern. The should_be_restful macro is like a super test generator, creating around 40-50 tests each time you call it. Here it is in its most simple incarnation:


class UsersControllerTest < Test::Unit::TestCase
  def setup
    @user = User.find(:first)
  end

  should_be_restful do |resource|
    resource.create.params = { :name => "Billy Bumbler", :party => 'Sure do!'}
    resource.update.params = { :name => "Changed" }
  end
end

This will create a set of tests for all of the CRUD actions, as well as for :new and :edit. There are a lot of ways to customize this macro to work with your controller, but it does a good job of figuring out sane defaults. In the above example, it looks at the test class name to figure out that it’s dealing with the User model, users_url methods, params[:user], @user and @users instance variables, etc.

Configuring should_be_restful

Let’s look at some of the ways you can change the behavior of the tests produced by should_be_restful. (This is all well documented in the rDocs as well)


class UsersControllerTest < Test::Unit::TestCase
  def setup
    @user = User.find(:first)
  end

  should_be_restful do |resource|
    resource.klass      = User
    resource.object     = :user
    resource.parent     = []
    resource.actions    = [:index, :show, :new, :edit, :update, :create, :destroy]
    resource.formats    = [:html, :xml]

    resource.create.params = { :name => "bob", :email => 'bob@bob.com', :age => 13}
    resource.update.params = { :name => "sue" }

    resource.create.redirect  = "user_url(@user)" 
    resource.update.redirect  = "user_url(@user)" 
    resource.destroy.redirect = "users_url" 

    resource.create.flash  = /created/i
    resource.update.flash  = /updated/i
    resource.destroy.flash = /removed/i
  end
end
  • klass: Constant for the model this resource deals with.
  • object: Name of the object that should be used for the #edit, #update and #destroy tests. So if you’ve instantiated @user in your setup, then res.object should be set to :user.
  • parent: Names of the parent objects. This is for figuring out the parameters to pass into a nested resource url helper. If you don’t have nested resources, don’t worry about it. If you have nested resources, with :users nested under :cities in your routes, then the res.parent for the user test should be set to :city. Actually, technically parent should refer to the name of the association on the User model. So if User belongs_to :location, class_name => "City", then you’d actually want to set it to :city.
  • actions: List of actions that you want to test. This can be a subset of [:index, :show, :new, :edit, :create, :update, :destroy] (the default), or just :all for all of those actions. If your controller is read_only, then you probably want to set this to [:index, :show].
  • formats: List of formats to test the controller against. This defaults to :html and :xml, and we hope to add support for JSON, CSV, and others soon. It should be pretty easy to add new formats… just look around the ThoughtBot::Shoulda::Controller::XML file for an example.

We’ve also got some parameters for the #create, #update, and #destroy tests:

  • params: What params to send into the action. These are tacked onto the params has like thus: post :create, :user => res.create.params
  • flash: What we expect to find in the flash after the action completes. This can be either a string or regex, which is matched against any of the flash values.
  • redirect: Where should we be redirected to after the action completes. This one is a little tricky… The parameter should be set to a string that is evaled at test runtime. The evaled string will have access to all of the named routes, and all of the instance variables created by the controller.

Denied actions

One of the most common scenarios in RESTful controller design is that some or all actions should be limited (maybe based on the logged in user). The pattern is so prolific that should_be_restful supports it natively with the resource.denied options:


class UsersControllerTest < Test::Unit::TestCase
  def setup
    @user = User.find(:first)
  end

  should_be_restful do |resource|
    resource.create.params = { :name => "bob", :email => 'bob@bob.com', :age => 13}
    resource.update.params = { :name => "sue" }

    resource.denied.actions  = [:new, :create, :edit, :update, :destroy]
    resource.denied.redirect = "new_session_url" 
    resource.denied.flash    = /administrator/i
  end
end

Every action listed in resource.denied.actions (empty by default) will be tested to ensure that the action is denied, and that the relevant records remain untouched. The :redirect and :flash params are the same as for the resource.create and resource.update counterparts above.

Mixing should_be_restful and contexts

Mixing the should_be_restful and context blocks is a very powerful way of testing each and every controller action against every security context. It’s common in our applications to have test files that look like this:


class PostsControllerTest < Test::Unit::TestCase
  def setup
    @user = users(:bob)
    @post = @user.posts.first
  end

  context "An administrator" do
    setup { login_as users(:admin) }

    should_be_restful do |resource|
      resource.create.params = { :subject => "test", :body => "message" }
      resource.update.params = { :subject => "changed" }
    end
  end

  context "A user" do
    setup { login_as @user }

    should_be_restful do |resource|
      resource.create.params = { :subject => "test", :body => "message" }
      resource.update.params = { :subject => "changed" }
    end
  end

  context "A stranger" do
    setup { login_as users(:sally) }

    should_be_restful do |resource|
      resource.create.params   = { :subject => "test", :body => "message" }
      resource.denied.actions  = [:edit, :update, :destroy]
      resource.denied.redirect = "login_url" 
      resource.denied.flash    = /only the owner can/i
    end
  end

  context "The public" do
    setup { logout }

    should_be_restful do |resource|
      resource.denied.actions  = [:new, :create, :edit, :update, :destroy]
      resource.denied.redirect = "signup_url" 
      resource.denied.flash    = /must be a member/i
    end
  end
end

Mixing and matching

Finally, it’s good to keep in mind that it’s perfectly fine to test extra actions on a controller along side the should_be_restful helper, or to have normal test_xxx methods alongside Shoulda:


class UsersControllerTest < Test::Unit::TestCase
  def setup
    @user = User.find(:first)
  end

  should_be_restful do |resource|
    resource.create.params = { :name => "Billy Bumbler", :party => 'Sure do!'}
    resource.update.params = { :name => "Changed" }
  end

  context "on GET to /users/:id;activate" 
    setup { get :activate, :id => @user.id }
    should_render_template :activate
    # etc.
  end

  def test_normal_stuff
    assert true
  end
end

Back to the tutorial