-
Notifications
You must be signed in to change notification settings - Fork 5.5k
How To: Test controllers with Rails (and RSpec)
Check the source for the current best practice regarding controller testing with Devise.
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
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 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.
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]
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
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.