Rails 6中如何让RSpec为读写数据库使用同一连接?
问题背景
从Rails 5迁移到Rails 6并引入读写分离数据库复制后,遇到请求测试(request specs)的矛盾问题:
- 单文件单独运行时,使用事务性测试夹具(transactional fixtures)可正常通过;
- 通过
bundle exec parallel_rspec批量运行(如Circle CI全量执行)时测试失败; - 关闭事务性夹具后测试能通过,但耗时大幅增加。
调试发现核心问题:测试数据写入主库(writer DB)后,仅能通过主库连接读取到,但接口的GET请求会自动切换到从库(reader DB)连接读取,此时从库返回0条数据——例如ActiveRecord::Base.connected_to(role: :reading) { puts Foo.count }结果为0,而用:writing角色执行则返回正确的非零值。
本质原因是事务性夹具的特性:测试数据不会提交到数据库,仅在写入时的连接可见,但请求测试会按请求类型自动切换数据库角色,导致从库无法读取未提交的事务内数据。
可行解决方案
1. 测试环境全局禁用读写分离切换
在config/environments/test.rb中添加配置,强制测试时所有数据库操作都走主库,跳过自动切换逻辑:
Rails.application.configure do # 其他测试环境配置... config.active_record.database_selector = nil config.active_record.database_resolver = nil config.active_record.database_resolver_context = nil end
此方式最简单高效,既保留事务性测试的速度,又避免读写角色切换带来的读取问题。
2. 给请求测试上下文绑定主库连接
如果需要保留部分场景的读写分离测试,可在请求测试的钩子中强制切换到主库,确保测试全程使用同一连接:
RSpec.describe "Foo API endpoints", type: :request do before do # 强制当前测试上下文使用主库连接 ActiveRecord::Base.connected_to(role: :writing) do # 在此块内创建测试数据,或直接让后续操作沿用主库连接 end end after do # 清理连接,避免影响其他测试 ActiveRecord::Base.connection_handler.clear_all_connections! end # 测试用例示例 it "returns the created foo record" do foo = Foo.create!(name: "test") get "/foos/#{foo.id}" expect(response.parsed_body["name"]).to eq("test") end end
3. 结合DatabaseCleaner实现事务隔离
针对并行测试的连接隔离问题,可使用database_cleaner配置事务策略,同时强制测试期间使用主库:
# spec/support/database_cleaner.rb RSpec.configure do |config| config.before(:suite) do DatabaseCleaner.strategy = :transaction DatabaseCleaner.clean_with(:truncation) end config.around(:each) do |example| DatabaseCleaner.cleaning do ActiveRecord::Base.connected_to(role: :writing) do example.run end end end end
这种方式既保证事务测试的高效性,又能解决并行测试时的连接隔离问题。
4. 测试环境主从库合一(进阶)
如果需要模拟真实读写分离逻辑,可在测试环境将主库和从库配置为同一个数据库(比如都指向测试用的PostgreSQL库),这样事务内的数据在读写角色下都能访问到。此方式适合需要验证读写切换逻辑但又不想放弃事务性测试的场景。
内容的提问来源于stack exchange,提问作者RonLugge

