Wednesday, May 13, 2009

Unit/Functional Test Rails ActionController filters following DRY

At ScrumPad most of our controllers are bounded by filters for authentication/authorization. Some filters apply to all actions in a controller while others apply to only a few or leave out only a few. However, since we are following TDD, we need to test the filter is invoked before each of the desired action. This makes the test code MOIST (not DRY)!

Example of Moist Code:

The following example only captures two test methods. However, if you have 30 controllers like ours and on an average 5 filters at each, you will possibly find many such duplicates making your test code so moist that it becomes shabby!

class SomeController
before_filter :authenticate
before_filter :restrict_invalid_role
end
class SomeControllerTest
def test_index_redirects_without_login
get :index
assert_redirected_to :controller=>:login, :action=>:login
end
def test_index_redirects_without_valid_role
login_as(:invalid_role)
get :index
assert_redirected_to :controller=>:exception, :action=>:not_allowed
end
end

Example of DRY Code:
I came up with the following implementation to help us with unit testing the before filters. The assumption is, if your filter is invoked, it will work fine. And we are testing the filter only once. The following code is written at the end of the test/test_helper.rb.
class ActionController::TestCase
##example: should_apply_before_filter_to_actions(:authenticate, [:index, :new])
def should_apply_before_filter_to_actions(before_filter_name, actions)
if(actions.nil? or actions.empty?)
assert false
end
filter = find_maching_before_filter(before_filter_name)
actions.each do |action|
assert_before_filter_applied(filter, action)
end
end
##example: should_apply_before_filter_to_action(:authenticate, :index)
def should_apply_before_filter_to_action(before_filter_name, action)
filter = find_maching_before_filter(before_filter_name)
assert_before_filter_applied(filter, action)
end

##example: should_not_apply_before_filter_to_actions(:authenticate, [:index, :new])
def should_not_apply_before_filter_to_actions(before_filter_name, actions)
if(actions.nil? or actions.empty?)
assert false
end
filter = find_maching_before_filter(before_filter_name)
actions.each do |action|
assert_before_filter_not_applied(filter, action)
end
end

##example: should_not_apply_before_filter_to_action(:authenticate, :index)
def should_not_apply_before_filter_to_action(before_filter_name, action)
filter = find_maching_before_filter(before_filter_name)
assert_before_filter_not_applied(filter, action)
end

##example: should_apply_before_filters([:authenticate, :session_expiry])
def should_apply_before_filters(before_filter_names)
if(before_filter_names.nil? or before_filter_names.empty?)
assert false, "No Before Filter is Passed"
end
before_filter_names.each {|filter| should_apply_before_filter(filter)}
end

##example: should_apply_before_filter(:authenticate)
def should_apply_before_filter(before_filter_name)
filter = find_maching_before_filter(before_filter_name)
if(filter.nil?)
assert false, "no matching filter found for #{before_filter_name}"
end
assert filter.options.empty?, "the filter #{before_filter_name} has only/except options and does not apply to all actions"
end

private
#finds the matching BeforeFilter object
def find_maching_before_filter(before_filter_name)
filters = @controller.class.filter_chain()
if !filters.nil?
filters.each do |filter|
if(filter.is_a?(ActionController::Filters::BeforeFilter) and filter.method == before_filter_name)
return filter
end
end
end
return nil
end

#asserts a single BeforeFilter is applied on a single action
def assert_before_filter_applied(filter, action)
if(filter.nil? or action.nil?)
assert false
end

if(filter.options.empty?)
assert true
return
end
if(!filter.options[:only].nil?)
assert filter.options[:only].include?(action.to_s)
end
if(!filter.options[:except].nil?)
assert !filter.options[:except].include?(action.to_s)
end
end

#asserts a single BeforeFilter is not applied on a single action
def assert_before_filter_not_applied(filter, action)
if(filter.nil? or action.nil?)
assert false
end

if(filter.options.empty?)
assert false
end

if(!filter.options[:except].nil?)
assert filter.options[:except].include?(action.to_s)
end
if(!filter.options[:only].nil?)
assert !filter.options[:only].include?(action.to_s)
end
end
end
Now my test code looks like the following-
  def test_filters
should_apply_before_filters(:authenticate, :restrict_invalid_role)
end
I can use the other methods as well to get finer control if the before_filter is applied using :only or :except options. And I can use these helper methods across all my controller test classes, making the whole point of testing filters really neat and short.

If you are familiar with shoulda tests, you will find the above code following shoulda like naming conventions. I found the above code to strip a lot of your efforts, since you eliminate all test codes that safeguard your filters.

Hope it helps someone with similar need.