如何使用RSpec模拟登录?

57
我已经使用Rails玩了几年了,并且制作了一些可接受的应用程序并投入生产。但我一直避免进行任何测试,现在我决定纠正这个问题。我试图为我为工作编写的一个已经上线但正在不断修改的应用程序编写一些测试。我担心任何更改都会破坏它,所以我想开始一些测试。我读过RSpec书籍,看过一些屏幕录像,但是很难入手(我觉得这是你只有真正做过才能理解的事情)。
我试图编写一个应该很简单的关于我的ReportsController的测试。我的应用程序的问题在于几乎整个应用程序都在认证层之后。如果未登录,则什么都不起作用,因此我必须模拟登录才能发送一个简单的get请求(虽然我想我应该编写一些测试来确保没有登录什么都不起作用-我稍后会处理这个问题)。
我使用RSpec、Capybara、FactoryGirl和Guard建立了一个测试环境(不确定要使用哪些工具,所以使用了Railscasts的建议)。到目前为止,我编写测试的方法是像这样在FactoryGirl中创建一个用户:
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

然后像这样编写我的测试:

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

这样会失败:

  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)>'

有趣的是,如果我将测试代码改为response.should be_redirect,测试就会通过,这表明在此之前一切都正常工作,但登录未被识别。

所以我的问题是,我需要做什么才能使此登录功能正常工作?我需要在数据库中创建与 FactoryGirl 凭证匹配的用户吗?如果是这样,那么 FactoryGirl 的作用是什么(我是否应该使用它)?我该如何在测试环境中创建这个虚拟用户?我的身份验证系统是一个非常简单的自制系统(基于 Railscasts 第 250 集)。几乎所有的测试都需要进行登录操作,那么我该怎样在我的代码中只进行一次登录,并使其适用于所有测试?

我知道这是一个大问题,感谢您审查。

5个回答

51
答案取决于您的身份验证实现。通常,当用户登录时,您会设置一个会话变量来记住该用户,例如 session[:user_id]。您的控制器将在before_filter中检查登录状态,并在不存在此类会话变量时进行重定向。我假设您已经在做类似这样的事情。
为了使测试正常工作,您必须手动将用户信息插入会话。以下是我们在工作中使用的部分代码:
# 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

现在,在我们的任何控制器示例中,我们可以调用login(some_user)来模拟以该用户身份登录。


我还应该提到,看起来您正在进行此控制器测试中的集成测试。作为一项规则,您的控制器测试只应模拟针对单个控制器操作的请求,例如:

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

这个测试专门针对单个控制器动作进行的,这正是你在一组控制器测试中所需要的。然后你可以使用Capybara/Cucumber来进行表单、视图和控制器的端到端集成测试。


1
太好了,谢谢。这是朝着正确方向的巨大推动。我已经开始实践了,但由于在测试环境中没有设置任何用户,所以仍然存在失败的测试。我应该如何设置它们?使用FactoryGirl吗?在测试环境中通过控制台创建它们?将现有的生产数据库复制到测试环境中? - brad
实际上,不用担心。我使用FactoryGirl这样做:“描述”GET 'index'“如下所示: 它“应该成功”做 用户= FactoryGirl.create(:user); 登录(用户); 得到:指数; 响应应该成功; 结束; 结束;。我不得不将request.session [:user]更改为request.session [:user_id],但除此之外,您代码中的所有内容都适用于我,现在我有了我的第一个通过测试!谢谢。 - brad
是的,您使用FactoryGirl创建用户的解决方案基本上也是我们所做的。如果它对您有用,请确保接受答案。谢谢! - Brandan
1
@Brandan 在控制器规范中使用 FactoryGirl 来访问数据库,这也算是集成测试,对吗? - hangsu
如何使用Rspec模拟登录 - Volkan

14

spec/support/controller_helpers.rb 中添加帮助程序文件,并复制下面的内容

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

现在在 spec/rails_helper.rbspec/spec_helper.rb 文件中添加以下行

require 'support/controller_helpers'

RSpec.configure do |config|

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

  end

现在在你的控制器规范文件中。

describe  "GET #index" do

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

Devise官方链接


我不知道为什么你的回答得到了“-1”,因为那是唯一一个对我实际起作用的,即使看起来有点“凌乱”;非常感谢你,伙计 ;) - Laurent
首先,出现了“undefined method create”的错误。如果这个人应该将“create”更改为其他内容,那么为什么不提一下呢?我是否也需要将“describe”或“sign_in”替换为其他内容?这个伪代码很令人困惑。 - Chris Vilches
2
这个问题与Devise无关。 - Askar

4

在功能测试中,使用Warden的helper #login_as是以用户身份登录的最简单方法。

login_as some_user

使用 gem 'devise' 或 'warden',这显然是最好的答案。 - Ri1a

2

因为我无法让@Brandan的答案起作用,但基于他和这篇文章,我得出了以下解决方案:

# 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

并且
# 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

那么在您的测试中,您可以使用以下代码:

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

0

对于不使用 Devise 的人:

spec/rails_helper.rb:

require_relative "support/login_helpers"

RSpec.configure do |config|
  config.include LoginHelpers
end

spec/support/login_helpers.rb:

module LoginHelpers
  def login_as(user)
    post "/session", params: { session: { email: user.email, password: "password" } }
  end
end

并且在规格中:

login_as(user)

这个在 before each 块中为什么不起作用? - fatfrog
你具体有什么错误?也许在before块中没有定义post - Dorian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接