Wolfmans Howlings

A programmers Blog about Ruby, Rails and a few other issues

RSpec testing all actions of a controller

Posted by Jim Morris Sat, 28 Jul 2007 21:23:07 GMT

A pattern I find very helpful is to find all the actions in a controller and apply a test to all those actions.

For instance this is useful for automatically testing all actions are protected from unauthorized access when using a login system.

One nice feature of this pattern is that if you add an action to a controller it will automatically be tested. This is less helpful if you use

before_filter :login_required, :except => {...}

as it will automatically be protected, but there are other use cases where this is not the situation. Just as in the except clause above you need to explicitly add any action that does not need to be tested to an exception list, which is supported by this pattern.

Here are the methods I use to test for login accessibility.

module MySpecHelper

  # get all actions for specified controller
  def get_all_actions(cont)
    c= Module.const_get(cont.to_s.pluralize.capitalize + "Controller")
    c.public_instance_methods(false).reject{ |action| ['rescue_action'].include?(action) }
  end

  # test actions fail if not logged in
  # opts[:exclude] contains an array of actions to skip
  # opts[:include] contains an array of actions to add to the test in addition
  # to any found by get_all_actions
  def controller_actions_should_fail_if_not_logged_in(cont, opts={})
    except= opts[:except] || []
    actions_to_test= get_all_actions(cont).reject{ |a| except.include?(a) }
    actions_to_test += opts[:include] if opts[:include]
    actions_to_test.each do |a|
      #puts "... #{a}"
      get a
      response.should_not be_success
      response.should redirect_to('http://test.host/login')
      flash[:warning].should == @login_warning
   end
 end
end

I put this in my spec_helper.rb and include it as shown here:

describe "When Logged out" do
  include MySpecHelper
  controller_name :events

  before(:each) do
    controller.stub!(:current_user).and_return(:false)
    @login_warning= "You need to be logged in to do that"
  end

  # test all actions require login except the ones specified
  # add new_comment as it is not seen by the automatic collector
  it "actions should fail" do
    controller_actions_should_fail_if_not_logged_in(:input, 
                              :except => ['index', 'show', 'tagged'], 
                              :include => ['new_comment'])
  end
end

The get_all_actions method collects all the public un-inherited methods in the given controller, these will consist of all the accessible actions in that controller. I explicitly exclude rescue_action as it is created by RSpec itself and should not be tested. Note it will not see any actions that are in application.rb so you need to add those to the list manually of you want them tested. (See the :include option in the example).

The controller_actions_should_fail_if_not_logged_in could be put in the spec itself rather than the spec_helper, but as I call this from all my controller specs it is more DRY to put it here. This method takes the controller name and an option array of actions names to ignore. This method tests all the actions and makes sure I get the expected result of the filter failing due to not being logged in.

I show an example spec that uses this to test my events controller, it mocks the login calls to say I am not logged in, and then tests them with the exceptions of the actions in this controller that do not require one to be logged in.

This pattern can be extended to test all sorts of things, and is especially useful for testing things where you can add an action and forget to do something in a filter to protect it. Make sure the default is on the side of caution though. IE you need to explicitly except actions rather than include actions.

Another example is something I recently stumbled upon in my RESTful controllers. In many cases it is good to use a verify statement to make sure that the RESTful actions actually can only be called with PUT, POST or DELETE and fail if called with GET. I use this statement in my controllers to enforce this...

# GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
verify :method => :put, :only => [ :update ], :add_flash => { :error => "Operation Failed" }, :redirect_to => { :action => :index }
verify :method => :post, :only => [ :create, :new_comment ], :add_flash => { :error => "Operation Failed" }, :redirect_to => { :action => :index }
verify :method => :delete, :only => [ :destroy ], :add_flash => { :error => "Operation Failed" }, :redirect_to => { :action => :index }

I test this in my specs using this in the MySpecHelper Module

  def controller_actions_should_fail_with_get(cont, except=[])
    actions_to_test= get_all_actions(cont).reject{ |a| except.include?(a) }
    actions_to_test.each do |a|
      #puts "... #{a}"
      get a
      response.should redirect_to("http://test.host/#{cont.to_s.pluralize}")
      flash[:error].should == 'Operation Failed'
    end
  end

and an example of its use in a spec...

  it "actions should fail if not post or put" do
    controller_actions_should_fail_with_get(:event, ['index', 'show', 'edit', 'new'])
  end                                                                                     

Now whenever I add an action, the default is that it will fail with a GET, unless I add it to the exclude list in the spec, this will remind me to check if the action required PUT, POST or DELETE instead and to add it to the verify if so or add it to the specs exclude list if not.

These automatic tests keep me honest, especially in the last case where you really don't want a GET to be able to delete something.

I hope this pattern is useful to you.

Posted in ,  | Tags , ,  | 12 comments | no trackbacks

Comments

  1. wolfmanjm said 9 minutes later:

    Of course this pattern can also be applied to regular rails tests as well as RSpec, and is particularly nice when used in integration tests.

  2. Matt Aimonetti said about 3 hours later:

    Interesting, that's a very smart way of quickly writing specs to verify that you setup your authentication properly. I'm just wondering when you would use the :include method? It seems to me that you might want to test all the actions except few. When do you need to include an action?

    Matt

  3. wolfmanjm said about 7 hours later:

    I use the :include option because I have an action in application.rb which can only be called when logged in so needs to be tested, but it will not be found by get_all_actions because that does not find inherited methods from the base class. So include is there to add those actions.

  4. Matti Kotsalainen said 16 days later:

    Thanks for this, it's really useful! I've got one problem with it though. I run into a bunch of 'No route match exceptions' since it tries to call my restful contollers with GET when they are only accessible through the other operations (because of the way the routes are setup in routes.rb).

  5. wolfmanjm said 16 days later:

    That shouldn't be too hard to fix, just create a map of actions to method, and call post, put or delete where appropriate with a default of get, which should throw an error if you add a new action.

    Another approach would be to test whatever is in your routes maps with the appropriate method.

  6. Matti Kotsalainen said 16 days later:

    I tried your second approach, ie checking the routes maps but I ran into trouble with actions that route like this:

    POST /users/:id/add_friend/ {:controller=>"users", :action=>"add_friend"}

    The routing greps for the id and if it isn't supplied, it won't route the request. I guess I'll have to go with your first approach.

  7. shaikriyaz@hexaware.com said about 1 month later:

    Hi can i have a sample rspec examples which depicts how to test one controller file. The rspec given above is a bit complex i felt as a beginner.

    Thanks in advance

  8. wolfmanjm said about 1 month later:

    To see examples of normal RSpec usage for a controller goto their web page, they have plenty of examples under the Rails section. This Blog is about testing all actions in a given controller,which is an advanced topic, and not everyone will need to do this.

  9. Dibi store said 2 months later:

    i use this for having all the actions of my controller: (Admin::UsersController.public_instance_methods - ApplicationController.public_instance_methods).each do |action|....

    I think is very simple, anyway your solution is cool

  10. Chris O'Sullivan said 4 months later:

    This is pretty fantastically sweet

    I did however have to change the get_all_actions method to use camelize rather than capitalize so that my multiword controllers would work.
    i.e:

    c= Module.const_get(cont.to_s.pluralize.capitalize + "Controller")

    becomes

    c= Module.const_get(cont.to_s.pluralize.pluralize + "Controller")

    So I can call the method with 'user_account' and it finds the appropriate UserAccountController

  11. brian said 6 months later:

    Does anyone have a example put that works on their controller? I can't get any updates to work using put. Get, and Post work fine. I also can't find any examples online that actually have the code that does it. This example uses puts .... and assumes that it works... I get a nil.downcase with no idea where/how to find it. The actually code manually tests fine and the services works from XML or the screens using html. Any put examples out there?

  12. eknok(maiklilangangemailaddressko@yahoo.com) said 9 months later:

    how could i test these codes?

    before_filter :login_required, :only => [:show, :edit, :update] skip_before_filter :store_location_filter after_filter(:create_referral_point, :only => :activate)

Trackbacks

Use the following link to trackback from your own site:
http://blog.wolfman.com/articles/trackback/336

(leave url/email »)

   Comment Markup Help Preview comment