Dane O'Connor
Dane O'Connor

Reputation: 77378

Test controllers without going through routing

I'm trying to test my controller's action chain in isolation. Specifically, I want to ensure my desired behavior is applied to all my controller's actions. For example, test that all my actions require authentication:

context "when not authenticated" do

  # single case
  describe "GET index" do
    it "responds with 401" do
      get :index
      response.code.should be(401)
    end
  end

  # all of them...
  described_class.action_methods.each do |action|
    ['get', 'put', 'post', 'delete', 'patch'].each do |verb|
      describe "#{verb.upcase} #{action}" do
        it "responds with 401" do
          send verb, action
          response.code.should == "401"
        end
      end
    end   
  end

end

I expected this to work but it doesn't. I get some ActionController::RoutingErrors. This is because some of my routes require params and in some cases I'm not supplying them (like when I call post :create). I get that. But what I don't understand is: why should it matter!?

For these tests routing is a separate concern. I care about my action chains, not my requests (that's what I have routing specs and request specs for). I shouldn't need to concern myself with my route constraints at this level.

So my question: Is there a way to test just the action chain without simulating a request?

EDIT: some research

It looks like routes are being exercised in TestCase#process. Is this necessary?

Upvotes: 3

Views: 2495

Answers (2)

Dane O'Connor
Dane O'Connor

Reputation: 77378

One work around is to loosen the routing engine's constraints. This doesn't bypass routing, but it does make it easier to work with for testing.

Add something like the following to your specs:

before(:all) do
  Rails.application.routes.draw { match ':controller(/:action)' }
end
after(:all) do
  Rails.application.reload_routes!
end

While not strictly an answer to the question, it might be a good enough work around.

Upvotes: 6

Kaleidoscope
Kaleidoscope

Reputation: 3627

I'd argue that routing is not a separate concern for controller specs. One reason why is that values are added to the params hash based on what values are passed into the url, and the code in your controller may depend on those values.

Anyway, I'm assuming that you have some kind of authorization method defined in your ApplicationController. Testing each controller individually seems a little redundant. Here's how I'd do it:

require "spec_helper"

describe ApplicationController do
  describe "require_current_user" do
    ACTIONS_AND_VERBS = [
      [:index,   :get],
      [:show,    :get],
      [:new,     :get],
      [:create,  :post],
      [:edit,    :get],
      [:update,  :put],
      [:destroy, :delete],
    ]

    controller do      
      ACTIONS_AND_VERBS.each do |action, _|
        define_method(action) do
        end
      end
    end

    ACTIONS_AND_VERBS.each do |action, verb|
      describe "#{verb.to_s.upcase} '#{action}'" do
        it "should be successful" do
          send(verb, action, id: -1)
          response.code.should eq("401")
        end
      end
    end
  end
end

And in my ApplicationController I'd have something like...

class ApplicationController < ActionController::Base
  protect_from_forgery

  before_filter :require_current_user

  def require_current_user
    head :unauthorized
  end
end

EDIT: If I understand correctly, what we're really testing is that your require_current_user, or whatever equivalent authorization process you want to occur, is working as expected. In that case, we can test just one action, and trust that before_filter works properly.

require "spec_helper"

describe ApplicationController do
  describe "require_current_user" do
    controller do
      def index
      end
    end

    it 'should head unauthorized for unauthorized users' do
      get :index
      response.code.should eq("401")
    end
  end
end

Upvotes: 2

Related Questions