Lost in Code.

Solving the mysterious ActionNotFound error when testing Devise controllers.

Let's say you're using Devise in your Rails app, and you need to override some behavior in SessionsController such that an attribute is set on the user record when the user logs in. Something like this:

class SessionsController < Devise::SessionsController
  def create
    resource = warden.authenticate!(:scope => resource_name, :recall => "new")
    resource.foo = "bar"
    set_flash_message :notice, :signed_in
    sign_in_and_redirect(resource_name, resource)
  end
end

So then you've got a controller test; maybe it looks like this:

describe SessionsController do
  describe 'POST #create' do
    it "sets the whatever" do
      user = Factory.create(:user, :email => "joe@bloe.com", :password => "secret")
      post :create, :user => {:email => "joe@bloe.com", :password => "secret"}
      user.reload
      user.foo.should == "bar"
    end
  end
end

So you go to run this test, and then you're clubbed upside the head with this monstrosity:

Failure/Error: post :create, :user => {:email => "joe@bloe.com", :password => "secret"}
AbstractController::ActionNotFound

(commence hair pulling, gnashing of teeth, etc.)

Breathe deep. The action does exist. You're not going crazy.


Where is that error coming from then? Well, it's actually Devise. See, Devise::Controllers::InternalHelpers#is_devise_resource?, which is added as a before_filter to every Devise controller, does this:

def is_devise_resource?
  raise ActionController::UnknownAction unless devise_mapping
end

Hmm, right. So what does #devise_mapping do? Let's just take a look:

def devise_mapping
  @devise_mapping ||= request.env["devise.mapping"]
end

Ah. That's the answer. In controller tests, request.env["devise.mapping"] doesn't seem to get set. So, to fix this, we just need to set this to something before each test:

before :each do
  request.env["devise.mapping"] = Devise.mappings[:user]
end

(If your resource isn't named "user", you obviously need to change that to the right thing. Consult your routes file if you forgot.)

What if we're going to be overriding multiple Devise controllers? Well, we can simply make a controller example group helper:

module ControllerExampleGroupMethods
  def it_behaves_like_a_devise_controller
    before :each do
      request.env["devise.mapping"] = Devise.mappings[:user]
    end
  end
end

RSpec.configure do |config|
  config.extend(ControllerExampleGroupMethods, :type => :controller)
end

Now we can use it in our controller spec like this 1:

describe SessionsController do
  it_behaves_like_a_devise_controller

  describe '#POST create' do
    # ...
  end
end

Problem solved!

  1. The reason why we can't simply use a shared example group is that the before(:each) will only take effect in the shared example group, when we want it to take effect in the group we're including it into.