Friday 31 May 2013

Testing Protected Methods in a Controller, Part 1

Methods in a controller (including applicatin_controller.rb) can be divided between those that are actions (so invoked by HTTP requests) and those that are not. Those that are not should all be set as protected (or private), and it is these that I am posting about.

The first thing to do is to shift as much as you can out of the controllers. If at all possible, put in in a model or a library, and then just unit test them. However, that is not always easy, say because you want to invoke a redirect from the method or you need quick access to the session (I expect you could handle the session in a library file, but I think it would be messy and more trouble than it is worth).

So we have a bunch of protected methods, and they all need testing.

While you could test them in the same file you test your actions, thematically they are very different, and I think it makes more sense to put them in their own file. Here is an example. The only tricky part is that you need to say what controller it should use with the "tests" command. Oh, and you need to use "send" to access the method being tested, as it is protected.

require 'test/test_helper'

class ControllerMethodsTest < ActionController::TestCase
  tests PostsController
  
  def setup
    user_setup
  end

  def test_current_user
    login_as ['trainer', 'it'], request
    assert_equal 'tester', @controller.send(:current_user).login
  end
end

The "user_setup" methods puts some roles into the testing database. The "login_as" method is one that I have used a lot for testing actions relating to web pages that have the user logged in.

  def login_as roles, request
    create_user
    user = UserLog::User.find_by_login 'tester'
    roles = [roles] unless roles.is_a? Array
    roles.each { |role| assign_role user, role }
    request.session[:user_id] = user.id
    #p "set request.session[:user_id] to #{request.session[:user_id]}"
  end

  def create_user name = "tester"
    user = UserLog::User.new
    user.login = name
    user.email = "#{name}@domain.com"
    user.username = name.titlecase
    user.password = "12345678"
    user.password_confirmation = "12345678"
    user.save(:validate => false)
    user.send(:activate!)
    UserLog::User.find_by_login name
  end

  def assign_role(user, rolename)
    role = UserLog::Role.find_by_rolename(rolename)
    raise "Failed to find role #{rolename}" if role.nil?
    permission = UserLog::Permission.new
    permission.role = role
    permission.user = user
    permission.save(:validate => false)
  end

See part 2 here.

Thursday 16 May 2013

When functional tests fail to fail

I have come across a couple of instances recently where the project passed the tests, but really should not have.


Check the record really was changed

If you move your models, controllers and views into sub-directories, form data will get the sub-directory name prepended to the parameters key

Parameters: {"utf8"=>"Ô£ô", "authenticity_token"=>"4", "post"=>{"name"=>"My Post", "text"=>"some text"}, "commit"=>"Submit", "id"=>"3"}

... becomes:
Parameters: {"utf8"=>"Ô£ô", "authenticity_token"=>"4", "subfolder_post"=>{"name"=>"My Post", "text"=>"some text"}, "commit"=>"Submit", "id"=>"3"}

Rails does that automatically, so you may not realise it has happened. In your controller, however, the action method is still looking for params[:post]. Rails does not change that, so when the user tries to edit a record, Rails finds no hash of data for params[:subfolder_post], so assumes there is nothing to change. Then it saves your record, and reports back that everything saved okay!

It gets worse. Your functional test might look like this:

  test "should update post" do
    id = Subfolder::Post.first.id
    put :update, :id => id, :post => { :text => 'modified' }
    assert_redirected_to subfolder_post(assigns(:post))
  end

So you know to change the path and the class name, but you forget to update the params key, because Rails did that for you. And the test passes!

The lesson here is to check the record really was changed:

 test "should update post" do
    id = Subfolder::Post.first.id
    put :update, :id => id, :post => { :text => 'modified' }
    assert_redirected_to subfolder_post(assigns(:post))
    assert_equal 'modified', Subfolder::Post.find(id).drum_text
  end

Check against different users

Another thing to test is that pages will work whether logged in or not. I have several pages that display a little differently depending on whether someone is logged in with a specific role - they see extra links or buttons to secure pages. I already had tests that check you have to be logged into those secure pages, but on the original pages, you do not need to be logged in, so no need to check that, right?

Except sometimes thing change, routes are removed or modified and links fail to work. But the tests do not catch that because I was only testing what a user who is not logged in sees.

The lesson here is to check any page with role-dependant output with both a user not logged in and as an admin with all roles.

Tuesday 14 May 2013

redirect_to :back

Long time since my last post I know, but I am mainly cruising along on what I already know with not much new to write about.


Quick trick I discovered today.

  def destroy
    Post.find(params[:id]).destroy
    flash[:notice] = 'Post deleted.'
    redirect_to :back
  end

The redirect_to :back part will send the user back to the page he was on. Useful if a post can be deleted from the post index page or from a user's page, and you want to send back to the right place.

In your functional test you need to set request.env["HTTP_REFERER"], as this is what :back will look at. You can then check it went there. Any strng will do.

 test "should destroy post" do
    request.env["HTTP_REFERER"] = 'origin'
    assert_difference('Post.count', -1) do
      delete :destroy, :id => @post.id
    end
    assert_redirected_to 'origin'
  end