Tuesday, 4 June 2013

Testing Protected Methods in a Controller, Part 2

See part 1 here.

Unit Testing Controller Methods

You are not obliged to test controller methods in with your functional tests. You can mix them in with your unit tests, though they do need a separate class (and so ideally a separate file). The trick is to sub-class from ActionController::TestCase, and to then nominate a controller you are testing (this is normally derived from the test name, so would not be necessary if you maintain the naming convention):

class DrumsControllerMethodsTest < ActionController::TestCase
  tests DrumLog::DrumsController

  # tests
end

Whether this is a good idea is debatable. I would argue that testing these protected methods is proper unit testing, and is not functional testing, so that suggests they should be with the rest of the unit tests (as your helper tests already are), however, I think I read that in Rails 4 they will be relabelled as model tests and controller tests, which kind of undermines that argument.

One solution is to move them to a library file (create a module, and include it in ApplicationController), then you can unit test them with your other library files. Obviously this works best for more generic methods.

Using A Mock Controller


Sometimes you want test methods that invoke methods like render and redirect_to that are doing all sorts of things behind the scenes. Better (in my opinion) to create a mock controller, and over-write these methods.

Here is an example. It is testing a method check_security_level (a filter in fact) that will send the user to the log-in page if he tries to access the edit page when not logged in, but does nothing if he tries to access the index page.

# Create a mock class to subvert redirect_to,
# inheriting from ApplicationController.
# You could inherit an actual controller class.
class MockSampleController < ApplicationController
  attr_reader :destination
  
  # Redefine direct_to to just note the destination and do nothing more.
  def redirect_to s; @destination = s; end
end



class MyControllerTest < ActionController::TestCase
  tests MockSampleController
  
  # First test, check_security_level should redirect the user to a login page
  def test_check_security_level_1
    # Set the request parameters (must be strings not symbols)
    @controller.params[:action] = 'edit'  
    @controller.send(:check_security_level)
    assert_equal '/user_log/sessions', @controller.destination[:controller]
  end

  # Second test, check_security_level should do nothing
  def test_check_security_level_2
    @controller.params[:action] = 'index'  
    @controller.send(:check_security_level)
    assert_nil @controller.destination
  end
end

Add New Routing For Tests


Sometimes you want to create a mock controller with its own set of actions for testing methods in your controllers. This is not trivial as you need to have a set of routes for your test actions, and it is bad practice to put them into routes.rb, as they will end up in your production environment.

Creating a routes.rb for testing is not ideal, as it will get out of sync with the real routes.rb, and there is no simple way to append new routes (the Application.routes.draw method clears all existing routes).

The solution is to use the with_routing method, which is a standad part of Rails. However, it is somewhat neglected, as the API shows...

This is the example from 2.3.8, when it was in  ActionController::TestProcess:
http://apidock.com/rails/ActionController/TestProcess/with_routing

 with_routing do |set|
    set.draw do |map|
      map.connect ':controller/:action/:id'
        assert_equal(
          ['/content/10/show', {}],
          map.generate(:controller => 'content', :id => 10, :action => 'show')
      end
    end
  end


Spot the missing right bracket to match the one in "assert_equal("?

In Rails 3 it got moved to ActionDispatch::Assertions::RoutingAssertions...

 with_routing do |set|
    set.draw do |map|
      map.connect ':controller/:action/:id'
        assert_equal(
          ['/content/10/show', {}],
          map.generate(:controller => 'content', :id => 10, :action => 'show')
      end
    end
  end

Still missing that right bracket. And still using Rails 2 style routing!

Here is an example that works:

  def test_render_generic_form
    with_routing do |set|
      set.draw do
        resources :mocks do
          collection do
            get :test_action
          end
        end
      end
    
    
      get :test_action
      # assertions
    end
  end


No comments: