Rails+Capybara+Selenium测试会话无法持久化问题排查
Rails+Capybara+Selenium会话丢失问题排查与解决
问题描述
在遗留Rails应用中切换Capybara驱动为Selenium后,单测试内出现会话丢失问题:
- 原使用rack_driver的测试可正常执行,切换为Selenium后,登录用户创建记录时,首次因字段无效提交失败,修正后再次提交时控制器抛出
User.current为nil的错误 - 已设置
use_transactional_tests = false,引入database-cleaner并配置:truncation清理策略,问题依旧 - 切换测试类继承
ActionDispatch::SystemTestCase后,问题未解决 - 测试中添加
binding.break并手动执行后续断言时,所有测试均可通过
测试代码
class ObservationFormIntegrationTest < CapybaraIntegrationTestCase def test_create_minimal_observation rolf = users("rolf") login!(rolf) assert(page.has_link?("Create Observation")) click_on("Create Observation") assert(page.has_selector?("body.observations__new")) within("#observation_form") do fill_in("naming_name", with: "Unrealistic name") fill_in("observation_place_name", with: locations.first.name) click_commit end assert(page.has_selector?("#name_messages", text: "We do not recognize the name")) assert_flash_warning assert_flash_text( :form_observations_there_is_a_problem_with_name.t.html_to_ascii ) assert(page.has_selector?("#observation_form")) within("#observation_form") do fill_in("naming_name", with: "Coprinus comatus") fill_in("observation_place_name", with: locations.first.name) click_commit end assert(page.has_selector?("body.observations__show")) assert_flash_success assert_flash_text(:runtime_observation_success.t.html_to_ascii) end end
控制器报错信息
Minitest::UnexpectedError: NoMethodError: undefined method `projects_member' for nil:NilClass User.current.projects_member(include: :observations).each do |project| ^^^^^^^^^^^^^^^^ app/controllers/observations_controller/edit_and_update.rb:163:in `update_projects' app/controllers/observations_controller/new_and_create.rb:200:in `save_everything_else' app/controllers/observations_controller/new_and_create.rb:116:in `create' app/controllers/application_controller.rb:240:in `catch_errors_and_log_request_stats'
测试辅助方法
# Login the given user in the current session. def login(login = users(:zero_user).login, password = "testpassword", remember_me = true, session: self) login = login.login if login.is_a?(User) # get the right user field session.visit("/account/login/new") session.within("#account_login_form") do session.fill_in("user_login", with: login) session.fill_in("user_password", with: password) session.check("user_remember_me") if remember_me == true session.first(:button, type: "submit").click end end # Login the given user, testing to make sure it was successful. def login!(user, *args, **kwargs) login(user, *args, **kwargs) session = kwargs[:session] || self assert_flash_success(session: session) user = User.find_by(login: user) if user.is_a?(String) assert_equal(user.id, User.current_id, "Wrong user ended up logged in!") end def assert_flash_text(text = "", session: self) session.assert_selector("#flash_notices") session.assert_selector("#flash_notices", text: text) end def assert_flash_warning(text = "", session: self) session.assert_selector("#flash_notices.alert-warning") assert_flash_text(text, session: session) if text end def assert_flash_success(text = "", session: self) session.assert_selector("#flash_notices.alert-success") assert_flash_text(text, session: session) if text end
Capybara集成测试配置
# Allow simuluation of user-browser interaction with capybara require("capybara/rails") require("capybara/minitest") require("database-cleaner/active_record") class CapybaraIntegrationTestCase < ActionDispatch::IntegrationTest # Make the Capybara DSL available in these integration tests include Capybara::DSL # Make `assert_*` methods behave like Minitest assertions include Capybara::Minitest::Assertions # Include our helpers include GeneralExtensions include FlashExtensions include CapybaraSessionExtensions include CapybaraMacros # Important to allow integration tests test the CSRF stuff to avoid unpleasant # surprises in production mode. def setup ApplicationController.allow_forgery_protection = true Capybara.default_max_wait_time = 2 # default in our test_helper = true. for capybara with selenium should be false self.use_transactional_tests = false # NOTE: Shouldn't be necessary, but in case: # Capybara.reset_sessions! # needed for selenium Capybara.server = :webrick Capybara.current_driver = :selenium_headless DatabaseCleaner.strategy = :truncation # :transaction :truncation DatabaseCleaner.start # Treat Rails html requests as coming from non-robots. Browser::Bot.any_instance.stubs(:bot?).returns(false) end def teardown Capybara.reset_sessions! Capybara.use_default_driver DatabaseCleaner.clean ApplicationController.allow_forgery_protection = false end end
原因分析
- 驱动会话机制差异:rack_driver直接共享测试进程的会话上下文,而Selenium是独立浏览器进程,完全依赖Cookie传递会话信息。如果应用的会话Cookie配置(如
domain、secure属性)与测试环境不匹配,浏览器无法正确保存或发送会话Cookie,导致控制器无法识别用户。 - 请求同步不足:测试中表单提交后未等待页面状态完全更新就执行后续操作,会话状态未在浏览器和服务器间同步完成。添加
binding.break时的手动等待相当于补足了同步时间,因此测试通过。 - DatabaseCleaner执行顺序错误:如果
DatabaseCleaner.start在Capybara驱动初始化之后执行,可能会意外清理掉登录用户的数据库记录,导致会话关联的用户不存在,进而User.current为nil。 - User.current实现依赖线程存储:如果
User.current是基于Thread.current存储,而非从会话Cookie中获取用户ID查询数据库,Selenium的独立进程无法共享测试线程的Thread.current数据,导致控制器无法获取当前用户。
解决方法
1. 修正会话Cookie配置
确保测试环境的config/initializers/session_store.rb中Cookie配置适配Selenium:
Rails.application.config.session_store :cookie_store, key: '_your_app_session', domain: :all, secure: Rails.env.production?, same_site: :lax
domain: :all允许Cookie在测试服务器的任意子域下生效secure: Rails.env.production?避免测试环境强制HTTPS导致Cookie无法发送same_site: :lax确保表单提交时Cookie能正常发送
2. 增加同步等待断言
在表单提交后添加明确的等待断言,确保页面状态完全同步:
比如在第一次提交失败后,新增等待表单重新可用的断言:
# 第一次提交失败后的断言 assert(page.has_selector?("#name_messages", text: "We do not recognize the name")) assert_flash_warning # 等待表单提交按钮重新可用,确保页面渲染完成 assert(page.has_selector?("#observation_form [type='submit']", disabled: false))
3. 调整DatabaseCleaner执行顺序
在setup方法中先启动DatabaseCleaner,再初始化Capybara驱动,避免清理已创建的测试数据:
def setup ApplicationController.allow_forgery_protection = true # 先启动DatabaseCleaner,确保测试数据在干净环境中创建 DatabaseCleaner.strategy = :truncation DatabaseCleaner.start Capybara.default_max_wait_time = 2 self.use_transactional_tests = false Capybara.server = :webrick Capybara.current_driver = :selenium_headless Browser::Bot.any_instance.stubs(:bot?).returns(false) end
4. 修复User.current的实现
如果User.current依赖Thread.current,修改为基于会话的方式:
在ApplicationController中添加前置钩子,从会话中获取用户:
before_action :set_current_user def set_current_user if session[:user_id] User.current = User.find_by(id: session[:user_id]) else User.current = nil end end
同时确保测试中仅通过浏览器登录流程维护会话,避免直接设置User.current。
5. 配置合法的User Agent
避免Selenium浏览器被识别为机器人,在Capybara驱动配置中设置标准User Agent:
Capybara.register_driver :selenium_headless do |app| options = Selenium::WebDriver::Chrome::Options.new options.add_argument('--headless=new') options.add_argument('--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36') Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) end
内容的提问来源于stack exchange,提问作者nimmolo
相关产品推荐
相关产品推荐

