You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

原因分析

  1. 驱动会话机制差异:rack_driver直接共享测试进程的会话上下文,而Selenium是独立浏览器进程,完全依赖Cookie传递会话信息。如果应用的会话Cookie配置(如domain、secure属性)与测试环境不匹配,浏览器无法正确保存或发送会话Cookie,导致控制器无法识别用户。
  2. 请求同步不足:测试中表单提交后未等待页面状态完全更新就执行后续操作,会话状态未在浏览器和服务器间同步完成。添加binding.break时的手动等待相当于补足了同步时间,因此测试通过。
  3. DatabaseCleaner执行顺序错误:如果DatabaseCleaner.start在Capybara驱动初始化之后执行,可能会意外清理掉登录用户的数据库记录,导致会话关联的用户不存在,进而User.current为nil。
  4. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 23:14:50