Rails 5.2 集成测试中的会话未持续存在
Sessions not persisting in Rails 5.2 integration tests
我有一个多租户应用程序,它使用 Apartment 进行 postgreSQL 模式,使用 Devise 进行用户身份验证。在我尝试编写一些集成测试之前,一切都运行顺利。
这是我目前所拥有的精简版(如需更多信息,请随时询问):
# test/support/sign_in_helper.rb
module SignInHelper
def sign_in_as(name)
sign_in users(name)
end
end
# test/test_helper.rb
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
#...
class ActionDispatch::IntegrationTest
include Devise::Test::IntegrationHelpers
include SignInHelper
end
# test/system/article/post_test.rb
require "application_system_test_case"
class Article::PostTest < ApplicationSystemTestCase
test 'post a new document' do
sign_in_as :will
visit articles_path
click_on 'Add New Article' # redirected to login page after clicking this button
fill_in 'Name', with: 'Hello world!'
fill_in 'Content', with: 'Yes yes'
click_on 'Create Article'
assert_select 'h1', /Hello world!/
end
end
articles_path
需要经过身份验证的用户,所以我知道登录助手可以正常工作。然而,每当我尝试转到另一个 link 时,用户突然未通过身份验证。
我像这样猴子修补了 Devise 的 authenticate_user!
方法:
def authenticate_user!(*args)
byebug
super
end
并确认 warden.authenticated?
为 articles_path
返回了 true
,但 false
随后尝试导航到 new_article_path
。
我在集成测试类型、控制器和系统中都注意到了这种行为。但是,在开发环境中使用此应用程序时,这不是问题。
最令人沮丧的是,我有一个不同的应用程序,它似乎具有与此应用程序相同的设置,但在测试时不会出现此身份验证问题。
我该如何调试这个问题?
系统
- Rails: 5.2.2
- 设计:4.5.0
- 水豚:3.13.2
更新 1(2019 年 2 月 4 日)
这是@BKSpurgeon 要求的文章控制器
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
before_action :set_article, only: [:show, :edit, :update, :archive]
def index
@articles = Article.where(archived: false)
end
def show
end
def new
@article = Article.new
end
def create
@article = Article.new(article_params)
if @article.save
redirect_to @article, notice: 'Your article was successfully created.'
else
flash[:error] = @article.errors.full_messages.to_sentence
render :new
end
end
def edit
end
def update
if @article.update(article_params)
redirect_to articles_path, notice: 'Your article was successfully updated.'
else
flash[:error] = @article.errors.full_messages.to_sentence
render :edit
end
end
def archive
if @article.archive!
redirect_to articles_path, notice: 'Your article was successfully archived.'
else
render :edit
end
end
private
def set_article
@article = Article.find(params[:id])
end
def article_params
params.require(:article).permit(:name, :content, :archived)
end
end
更新 2(2019 年 2 月 4 日)
我写了一个简单的中间件,放在Warden之前调试:
# lib/debug_warden_middleware.rb
class DebugWardenMiddleware
def initialize(app)
@app = app
end
def call(env)
@status, @headers, @response = @app.call(env)
if env['warden'].present?
puts "User (#{env['warden'].user.present?}), Class: #{@response.class.name}"
end
return [@status, @headers, @response]
end
end
# config/application.rb
#...
module AppName
class Application < Rails::Application
# ...
config.middleware.insert_before Warden::Manager, DebugWardenMiddleware
end
end
而且我注意到 warden 似乎会在每次请求(包括资产请求)后清除其用户:
bin/rails test:system
Run options: --seed 39763
# Running:
Capybara starting Puma...
* Version 3.9.1 , codename: Private Caller
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:57466
User (true), Uri: ActionDispatch::Response::RackBody
User (false), Uri: Sprockets::Asset
User (false), Uri: Sprockets::Asset
User (false), Uri: Sprockets::Asset
User (false), Uri: ActionDispatch::Response::RackBody
User (false), Uri: ActionDispatch::Response::RackBody
User (false), Uri: Sprockets::Asset
[Screenshot]: tmp/screenshots/failures_test_post_a_new_article.png
E
Error:
Agenda::PostTest#test_post_a_new_article:
Capybara::ElementNotFound: Unable to find field "Name"
test/system/article/post_test.rb:9:in `block in <class:PostTest>'
bin/rails test test/system/article/post_test.rb:4
附带说明一下,我同时使用 Sprockets 和 Webpacker。
好吧,在 @BKSpurgeon (in a chat message) and @Old Pro 建议我尝试在单独的应用程序中重现问题与大家分享后,我偶然发现了答案。
因为这个应用程序是多租户的并且使用公寓 gem,所以我的测试设置有点复杂。不幸的是,我没有包括其中的一些设置,认为它是无关紧要的 (big mistake, huge!)。直到我尝试重新创建我的应用程序时,我才注意到这个缺陷。这是我使用的测试设置:
# test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
require 'rails/test_help'
# support files
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
# Apartment setup
Apartment::Tenant.drop('sample') rescue nil
Apartment::Tenant.create('sample') rescue nil
Apartment::Tenant.switch! 'sample'
# Capybara setup
Capybara.configure do |config|
config.default_host = 'http://lvh.me'
config.always_include_port = true
end
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
# Add more helper methods to be used by all tests here...
end
class ActionDispatch::IntegrationTest
include Devise::Test::IntegrationHelpers
include SubdomainHelper
include SignInHelper # dependent on SubomainHelper
setup do
default_url_options[:host] = 'lvh.me'
Apartment::Tenant.switch! 'public'
end
end
...
# test/support/sign_in_helper.rb
module SignInHelper
def sign_in_as(name)
# This right here was the problem code
user = users(name)
use_account user.account
sign_in user
end
end
...
# test/support/subdomain_helper.rb
module SubdomainHelper
def use_account(account)
account.save unless account.persisted?
default_url_options[:host] = "#{account.subdomain}.lvh.me"
Capybara.app_host = "http://#{account.subdomain}.lvh.me"
Apartment::Tenant.switch! account.subdomain
end
end
...
# test/system/article/post_test.rb
require "application_system_test_case"
class Article::PostTest < ApplicationSystemTestCase
test 'post a new document' do
sign_in_as :will
visit articles_path
click_on 'Add New Article' # redirected to login page after clicking this button
fill_in 'Name', with: 'Hello world!'
fill_in 'Content', with: 'Yes yes'
click_on 'Create Article'
assert_select 'h1', /Hello world!/
end
end
最终的问题是,在我实际切换到包含该用户的租户之前,我试图在 SignInHelper
中获取用户。有了这些知识,我将代码更改为如下所示:
# test/support/sign_in_helper.rb
module SignInHelper
def sign_in_as(name, account)
use_account accounts(account)
sign_in users(name)
end
end
...
# test/system/article/post_test.rb
require "application_system_test_case"
class Article::PostTest < ApplicationSystemTestCase
test 'post a new document' do
sign_in_as :will, :sample
# ...
end
end
现在会话持续存在。仍然让我感到困惑的一件事是,为什么我能够登录?我已将其归因于挥之不去的测试数据,但这更像是一种猜测。
我有一个多租户应用程序,它使用 Apartment 进行 postgreSQL 模式,使用 Devise 进行用户身份验证。在我尝试编写一些集成测试之前,一切都运行顺利。
这是我目前所拥有的精简版(如需更多信息,请随时询问):
# test/support/sign_in_helper.rb
module SignInHelper
def sign_in_as(name)
sign_in users(name)
end
end
# test/test_helper.rb
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
#...
class ActionDispatch::IntegrationTest
include Devise::Test::IntegrationHelpers
include SignInHelper
end
# test/system/article/post_test.rb
require "application_system_test_case"
class Article::PostTest < ApplicationSystemTestCase
test 'post a new document' do
sign_in_as :will
visit articles_path
click_on 'Add New Article' # redirected to login page after clicking this button
fill_in 'Name', with: 'Hello world!'
fill_in 'Content', with: 'Yes yes'
click_on 'Create Article'
assert_select 'h1', /Hello world!/
end
end
articles_path
需要经过身份验证的用户,所以我知道登录助手可以正常工作。然而,每当我尝试转到另一个 link 时,用户突然未通过身份验证。
我像这样猴子修补了 Devise 的 authenticate_user!
方法:
def authenticate_user!(*args)
byebug
super
end
并确认 warden.authenticated?
为 articles_path
返回了 true
,但 false
随后尝试导航到 new_article_path
。
我在集成测试类型、控制器和系统中都注意到了这种行为。但是,在开发环境中使用此应用程序时,这不是问题。
最令人沮丧的是,我有一个不同的应用程序,它似乎具有与此应用程序相同的设置,但在测试时不会出现此身份验证问题。
我该如何调试这个问题?
系统
- Rails: 5.2.2
- 设计:4.5.0
- 水豚:3.13.2
更新 1(2019 年 2 月 4 日)
这是@BKSpurgeon 要求的文章控制器
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
before_action :set_article, only: [:show, :edit, :update, :archive]
def index
@articles = Article.where(archived: false)
end
def show
end
def new
@article = Article.new
end
def create
@article = Article.new(article_params)
if @article.save
redirect_to @article, notice: 'Your article was successfully created.'
else
flash[:error] = @article.errors.full_messages.to_sentence
render :new
end
end
def edit
end
def update
if @article.update(article_params)
redirect_to articles_path, notice: 'Your article was successfully updated.'
else
flash[:error] = @article.errors.full_messages.to_sentence
render :edit
end
end
def archive
if @article.archive!
redirect_to articles_path, notice: 'Your article was successfully archived.'
else
render :edit
end
end
private
def set_article
@article = Article.find(params[:id])
end
def article_params
params.require(:article).permit(:name, :content, :archived)
end
end
更新 2(2019 年 2 月 4 日)
我写了一个简单的中间件,放在Warden之前调试:
# lib/debug_warden_middleware.rb
class DebugWardenMiddleware
def initialize(app)
@app = app
end
def call(env)
@status, @headers, @response = @app.call(env)
if env['warden'].present?
puts "User (#{env['warden'].user.present?}), Class: #{@response.class.name}"
end
return [@status, @headers, @response]
end
end
# config/application.rb
#...
module AppName
class Application < Rails::Application
# ...
config.middleware.insert_before Warden::Manager, DebugWardenMiddleware
end
end
而且我注意到 warden 似乎会在每次请求(包括资产请求)后清除其用户:
bin/rails test:system
Run options: --seed 39763
# Running:
Capybara starting Puma...
* Version 3.9.1 , codename: Private Caller
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:57466
User (true), Uri: ActionDispatch::Response::RackBody
User (false), Uri: Sprockets::Asset
User (false), Uri: Sprockets::Asset
User (false), Uri: Sprockets::Asset
User (false), Uri: ActionDispatch::Response::RackBody
User (false), Uri: ActionDispatch::Response::RackBody
User (false), Uri: Sprockets::Asset
[Screenshot]: tmp/screenshots/failures_test_post_a_new_article.png
E
Error:
Agenda::PostTest#test_post_a_new_article:
Capybara::ElementNotFound: Unable to find field "Name"
test/system/article/post_test.rb:9:in `block in <class:PostTest>'
bin/rails test test/system/article/post_test.rb:4
附带说明一下,我同时使用 Sprockets 和 Webpacker。
好吧,在 @BKSpurgeon (in a chat message) and @Old Pro 建议我尝试在单独的应用程序中重现问题与大家分享后,我偶然发现了答案。
因为这个应用程序是多租户的并且使用公寓 gem,所以我的测试设置有点复杂。不幸的是,我没有包括其中的一些设置,认为它是无关紧要的 (big mistake, huge!)。直到我尝试重新创建我的应用程序时,我才注意到这个缺陷。这是我使用的测试设置:
# test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
require 'rails/test_help'
# support files
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
# Apartment setup
Apartment::Tenant.drop('sample') rescue nil
Apartment::Tenant.create('sample') rescue nil
Apartment::Tenant.switch! 'sample'
# Capybara setup
Capybara.configure do |config|
config.default_host = 'http://lvh.me'
config.always_include_port = true
end
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
# Add more helper methods to be used by all tests here...
end
class ActionDispatch::IntegrationTest
include Devise::Test::IntegrationHelpers
include SubdomainHelper
include SignInHelper # dependent on SubomainHelper
setup do
default_url_options[:host] = 'lvh.me'
Apartment::Tenant.switch! 'public'
end
end
...
# test/support/sign_in_helper.rb
module SignInHelper
def sign_in_as(name)
# This right here was the problem code
user = users(name)
use_account user.account
sign_in user
end
end
...
# test/support/subdomain_helper.rb
module SubdomainHelper
def use_account(account)
account.save unless account.persisted?
default_url_options[:host] = "#{account.subdomain}.lvh.me"
Capybara.app_host = "http://#{account.subdomain}.lvh.me"
Apartment::Tenant.switch! account.subdomain
end
end
...
# test/system/article/post_test.rb
require "application_system_test_case"
class Article::PostTest < ApplicationSystemTestCase
test 'post a new document' do
sign_in_as :will
visit articles_path
click_on 'Add New Article' # redirected to login page after clicking this button
fill_in 'Name', with: 'Hello world!'
fill_in 'Content', with: 'Yes yes'
click_on 'Create Article'
assert_select 'h1', /Hello world!/
end
end
最终的问题是,在我实际切换到包含该用户的租户之前,我试图在 SignInHelper
中获取用户。有了这些知识,我将代码更改为如下所示:
# test/support/sign_in_helper.rb
module SignInHelper
def sign_in_as(name, account)
use_account accounts(account)
sign_in users(name)
end
end
...
# test/system/article/post_test.rb
require "application_system_test_case"
class Article::PostTest < ApplicationSystemTestCase
test 'post a new document' do
sign_in_as :will, :sample
# ...
end
end
现在会话持续存在。仍然让我感到困惑的一件事是,为什么我能够登录?我已将其归因于挥之不去的测试数据,但这更像是一种猜测。