How do I simulate a login with RSpec?

36,255

Solution 1

The answer depends on your authentication implementation. Normally, when a user logs in, you'll set a session variable to remember that user, something like session[:user_id]. Your controllers will check for a login in a before_filter and redirect if no such session variable exists. I assume you're already doing something like this.

To get this working in your tests, you have to manually insert the user information into the session. Here's part of what we use at work:

# spec/support/spec_test_helper.rb
module SpecTestHelper   
  def login_admin
    login(:admin)
  end

  def login(user)
    user = User.where(:login => user.to_s).first if user.is_a?(Symbol)
    request.session[:user] = user.id
  end

  def current_user
    User.find(request.session[:user])
  end
end

# spec/spec_helper.rb
RSpec.configure do |config|
  config.include SpecTestHelper, :type => :controller
end

Now in any of our controller examples, we can call login(some_user) to simulate logging in as that user.


I should also mention that it looks like you're doing integration testing in this controller test. As a rule, your controller tests should only be simulating requests to individual controller actions, like:

it 'should be successful' do
  get :index
  response.should be_success
end

This specifically tests a single controller action, which is what you want in a set of controller tests. Then you can use Capybara/Cucumber for end-to-end integration testing of forms, views, and controllers.

Solution 2

Add helper file in spec/support/controller_helpers.rb and copy content below

module ControllerHelpers
    def sign_in(user)
      if user.nil?
        allow(request.env['warden']).to receive(:authenticate!).and_throw(:warden, {:scope => :user})
        allow(controller).to receive(:current_user).and_return(nil)
      else
        allow(request.env['warden']).to receive(:authenticate!).and_return(user)
        allow(controller).to receive(:current_user).and_return(user)
      end
    end
  end

Now add following lines in spec/rails_helper.rb or spec/spec_helper.rb file

require 'support/controller_helpers'

RSpec.configure do |config|

    config.include Devise::TestHelpers, :type => :controller
    config.include ControllerHelpers, :type => :controller

  end

Now in your controller spec file.

describe  "GET #index" do

    before :each do        
        @user=create(:user)
        sign_in @user
    end
      ...
end

Devise Official Link

Solution 3

As I couldn't make @Brandan's answer work, but based on it and on this post, I've came to this solution:

# spec/support/rails_helper.rb
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } # Add this at top of file

...

include ControllerMacros # Add at bottom of file

And

# spec/support/controller_macros.rb
module ControllerMacros

  def login_as_admin
    admin = FactoryGirl.create(:user_admin)
    login_as(admin)
  end

  def login_as(user)
    request.session[:user_id] = user.id
  end

end

Then on your tests you can use:

it "works" do
  login_as(FactoryGirl.create(:user))
  expect(request.session[:user_id]).not_to be_nil
end

Solution 4

The easiest way to login with a user on feature tests is to use the Warden's helper #login_as

login_as some_user
Share:
36,255
brad
Author by

brad

I'm a doctor, not an engineer!

Updated on July 09, 2022

Comments

  • brad
    brad almost 2 years

    I have been playing with Rails for a couple of years now and have produced a couple of passable apps that are in production. I've always avoided doing any testing though and I have decided to rectify that. I'm trying to write some tests for an app that I wrote for work that is already up and running but undergoing constant revision. I'm concerned that any changes will break things so I want to get some tests up and running. I've read the RSpec book, watched a few screencasts but am struggling to get started (it strikes me as the sort of thing you only understand once you've actually done it).

    I'm trying to write what should be a simple test of my ReportsController. The problem with my app is that pretty much the entire thing sits behind an authentication layer. Nothing works if you're not logged in so I have to simulate a login before I can even send forth a simple get request (although I guess I should write some tests to make sure that nothing works without a login - I'll get to that later).

    I've set up a testing environment with RSpec, Capybara, FactoryGirl and Guard (wasn't sure which tools to use so used Railscasts' suggestions). The way I've gone about writing my test so far is to create a user in FactoryGirl like so;

    FactoryGirl.define do
      sequence(:email) {|n| "user#{n}@example.com"}
      sequence(:login) {|n| "user#{n}"}
      factory :user do
        email {FactoryGirl.generate :email}
        login {FactoryGirl.generate :login}
        password "abc"
        admin false
        first_name "Bob"
        last_name "Bobson"
      end
    end
    

    and then write my test like so;

    require 'spec_helper'
    
    describe ReportsController do
      describe "GET 'index'" do
        it "should be successful" do
          user = Factory(:user)
          visit login_path
          fill_in "login", :with => user.login
          fill_in "password", :with => user.password
          click_button "Log in"
          get 'index'
          response.should be_success
        end
      end
    end
    

    This fails like so;

      1) ReportsController GET 'index' should be successful
         Failure/Error: response.should be_success
           expected success? to return true, got false
         # ./spec/controllers/reports_controller_spec.rb:13:in `block (3 levels) in <top (required)>'
    

    Interestingly if I change my test to response.should be_redirect, the test passes which suggests to me that everything is working up until that point but the login is not being recognised.

    So my question is what do I have to do to make this login work. Do I need to create a user in the database that matches the FactoryGirl credentials? If so, what is the point of FactoryGirl here (and should I even be using it)? How do I go about creating this fake user in the testing environment? My authentication system is a very simple self-made one (based on Railscasts episode 250). This logging in behaviour will presumably have to replicated for almost all of my tests so how do I go about doing it once in my code and having it apply everywhere?

    I realise this is a big question so I thank you for having a look.

  • brad
    brad about 12 years
    Great, thanks. That's a huge push in the right direction. I've put this into practice and still have a failing test because I don't have any users set up in the test environment. How should I go about setting them up? Use FactoryGirl? Create them in the console with a testing environment? Copy across an existing production database to the testing environment?
  • brad
    brad about 12 years
    Actually, don't worry. I managed to do it with FactoryGirl like so; describe "GET 'index'" do; it "should be successful" do; user = FactoryGirl.create(:user); login(user); get :index; response.should be_success; end; end;. I had to change request.session[:user] to request.session[:user_id] but apart from that everything in your code worked for me and I now have my first passing test! Thankyou.
  • Brandan
    Brandan about 12 years
    Yes, your solution using FactoryGirl to create the user is essentially what we do too. Please make sure to accept the answer if it works for you. Thanks!
  • hangsu
    hangsu about 10 years
    @Brandan hitting the database using FactoryGirl in a controller spec also makes it integration testing, no?
  • Laurent
    Laurent about 8 years
    I don't know why your answer got -1 because that's the ONLY one which actually worked for me even it looks a bit "messy" ; thanks a lot man ;)
  • Chris Vilches
    Chris Vilches over 6 years
    First of all, undefined method create. If the person was supposed to change create to something else, then why not mention it? Should I also replace describe or sign_in to something else? this pseudo-code is confusing.
  • Askar
    Askar about 5 years
    The question is not related to Devise
  • Ri1a
    Ri1a over 3 years
    Using the gem 'devise' or 'warden' this is clearly the best answer.
  • fatfrog
    fatfrog over 3 years
    Is there a reason this doesn't work in a before each block?
  • Dorian
    Dorian over 3 years
    what error do you have exactly? maybe post is not defined in before blocks
  • Volkan
    Volkan over 2 years