Skip to content

How To: Test controllers with Rails 3 and 4 (and RSpec)

Daniel Ambrosio edited this page Jun 10, 2015 · 37 revisions

First, be sure to speed up your tests!

Controller tests (Test::Unit)

To sign in as admin for a given test case, just do:

class SomeControllerTest < ActionController::TestCase
  include Devise::TestHelpers

  def setup
    @request.env["devise.mapping"] = Devise.mappings[:admin]
    sign_in FactoryGirl.create(:admin)
  end
end

Note: If you are using the confirmable module, you should set a confirmed_at date inside the Factory or call confirm! before sign_in.

Here is the basics to prepare inside your Factory:

FactoryGirl.define do
  factory :account do
    email { Faker::Internet.email }
    password "password"
    password_confirmation "password"
    confirmed_at Date.today
  end
end

Controller specs

Controller specs won't work out of the box if you're using any of devise's utility methods.

As of rspec-rails-2.0.0 and devise-1.1, the best way to put devise in your specs is simply to add the following into spec_helper:

require 'devise'

RSpec.configure do |config|
  config.include Devise::TestHelpers, :type => :controller
end

Note: If this include is done before your require 'rspec/rails', then move this include to spec/rails_helper.rb

You can also write to controller_macros_spec.rb file inside spec/support which contains the following:

module ControllerMacros
  def login_admin
    before(:each) do
      @request.env["devise.mapping"] = Devise.mappings[:admin]
      sign_in FactoryGirl.create(:admin) # Using factory girl as an example
    end
  end

  def login_user
    before(:each) do
      @request.env["devise.mapping"] = Devise.mappings[:user]
      user = FactoryGirl.create(:user)
      user.confirm! # or set a confirmed_at inside the factory. Only necessary if you are using the "confirmable" module
      sign_in user
    end
  end
end

Note: If your admin factory is nested on your user factory, you'll need to call sign_in like this:

  def login_admin
    before(:each) do
      @request.env["devise.mapping"] = Devise.mappings[:admin]
      admin = FactoryGirl.create(:admin)
      sign_in :user, admin # sign_in(scope, resource)
    end
  end

Then in spec/spec_helper.rb or spec/support/devise.rb:

RSpec.configure do |config|
  config.include Devise::TestHelpers, :type => :controller
  config.extend ControllerMacros, :type => :controller
end

So now in your controller specs, you can now do:

describe MyController do
  login_admin

  it "should have a current_user" do
    # note the fact that you should remove the "validate_session" parameter if this was a scaffold-generated controller
    subject.current_user.should_not be_nil
  end

  it "should get index" do
    # Note, rails 3.x scaffolding may add lines like get :index, {}, valid_session
    # the valid_session overrides the devise login. Remove the valid_session from your specs
    get 'index'
    response.should be_success
  end
end

Mappings

Every time you want to unit test a devise controller, you need to tell Devise which mapping to use. We need that because ActionController::TestCase and spec/controllers bypass the router and it is the router that tells Devise which resource is currently being accessed, you can do that with:

@request.env["devise.mapping"] = Devise.mappings[:admin]

Authenticated routes in Rails 3

If you choose to authenticate in routes.rb, you lose the ability to test your routes via assert_routing (which combines assert_recognizes and assert_generates, so you lose them also). It's a limitation in Rails: Rack runs first and checks your routing information but since functional/controller tests run at the controller level, you cannot provide authentication information to Rack which means request.env['warden'] is nil and Devise generates one of the following errors:

NoMethodError: undefined method 'authenticate!' for nil:NilClass
NoMethodError: undefined method 'authenticate?' for nil:NilClass

The solution is to test authenticated routes in the controller tests. To do this, stub out your authentication methods for the controller test, as described here: How-To: Stub authentication in controller specs

Rails 3 RSpec scaffolds

If you're using the default Rspec scaffold generator, the generated controller specs pass along session parameters:

get :index, {}, valid_session

These are overwriting the session variables that Devise's helpers set to sign in with Warden. The simplest solution is to remove them:

get :index, {}

Alternatively, you could set the Warden session information in them manually, instead of using Devise's helpers.

credit: Ian Terrell's answer on stackoverflow

Clone this wiki locally