Wolfmans Howlings

A programmers Blog about Programming solutions and a few other issues

RSpec testing all actions of a controller

Posted by Jim Morris on Sat Jul 28 14:23:07 -0700 2007

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.

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

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...

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

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

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 Rails,RSpec  |  Tags rails,rspec,controllers  |  14 comments

Comments

  1. wolfmanjm said on Sat Jul 28 14:31:43 -0700 2007
    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 on Sat Jul 28 17:36:00 -0700 2007
    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 on Sat Jul 28 21:43:20 -0700 2007
    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 on Mon Aug 13 14:54:17 -0700 2007
    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 on Mon Aug 13 15:09:25 -0700 2007
    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 on Tue Aug 14 07:07:46 -0700 2007
    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 on Sat Sep 01 07:59:51 -0700 2007
    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 on Sat Sep 01 15:24:11 -0700 2007
    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 on Sun Oct 21 06:13:24 -0700 2007
    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 on Tue Dec 11 06:38:18 -0800 2007
    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 on Mon Feb 04 14:16:53 -0800 2008
    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 on Mon May 19 01:20:45 -0700 2008
    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)`
  13. aprilaire said on Sat Apr 24 17:55:52 -0700 2010
    @Brian, having the same issues. I continue to get a nil.downcase repeatedly and don't have a clue where it's at. Testing good, still no solutions.
  14. Slobodan Kovacevic said on Fri Mar 11 04:46:00 -0800 2011
    In newer Rails/rspec versions (Rails 3, rspec 2.5) you can replace get_all_actions call with controller_class.action_methods

(leave email »)