Skip to content

How To: Test controllers with Rails (and RSpec)

Lev Cheryomukhin edited this page Aug 24, 2020 · 7 revisions

Check the source for the current best practice regarding controller testing with Devise.

Controller tests (Minitest)

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

class SomeControllerTest < ActionController::TestCase
  # For Devise >= 4.2.0
  include Devise::Test::ControllerHelpers
  # Use the following instead if you are on Devise <= 4.2.0
  # include Devise::TestHelpers

  def setup
    @request.env["devise.mapping"] = Devise.mappings[:admin]
    sign_in FactoryBot.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:

# If your model is called User, then use :user instead of :account below:

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

Integration tests (Minitest)

If you are using integration tests, to simulate a login, you can use the following:

class SomeIntegrationTest < ActionDispatch::IntegrationTest
  include Devise::Test::IntegrationHelpers

  def setup
    sign_in FactoryBot.create(:user)
  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/rails_helper.rb:

require 'spec_helper'
require 'rspec/rails'
# note: require 'devise' after require 'rspec/rails'
require 'devise'

RSpec.configure do |config|
  # For Devise > 4.1.1
  config.include Devise::Test::ControllerHelpers, type: :controller
  config.include Devise::Test::IntegrationHelpers, type: :request
  # Use the following instead if you are on Devise <= 4.1.1
  # config.include Devise::TestHelpers, :type => :controller
end

You can also write to controller_macros.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 FactoryBot.create(:admin) # Using factory bot as an example
    end
  end

  def login_user
    before(:each) do
      @request.env["devise.mapping"] = Devise.mappings[:user]
      user = FactoryBot.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 = FactoryBot.create(:admin)
      sign_in :user, admin # sign_in(scope, resource)
    end
  end

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

require_relative 'support/controller_macros' # or require_relative './controller_macros' if write in `spec/support/devise.rb`

RSpec.configure do |config|
  # For Devise > 4.1.1
  config.include Devise::Test::ControllerHelpers, :type => :controller
  # Use the following instead if you are on Devise <= 4.1.1
  # 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
    expect(subject.current_user).to_not eq(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

Note: Remember to explicitly add the require command in your controller spec file to load the support files. RSpec does not automatically load the files in the support folder anymore.

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