使用Rspec&FactoryBot测试带条件作用域的has_many关联时遇异常
Rails关联测试异常:带条件的has_many集合为空但计数正确
环境信息
- Ruby 2.7
- Rails 6.0
- Rspec 3.12
- FactoryBot 6.2
模型代码
class Club < ApplicationRecord has_many :members, inverse_of: :club, dependent: :nullify, autosave: true has_many :active_members, -> { where(active_status_id: [1, 2]) }, inverse_of: :club, dependent: :nullify, autosave: true end class Member < ApplicationRecord belongs_to :active_status, inverse_of: :members belongs_to :club, inverse_of: :members, optional: true end
FactoryBot配置
FactoryBot.define do factory :club do end end FactoryBot.define do factory :member do end end
测试代码
RSpec.describe Club, type: :model do before do @club = create(:club) end describe 'clubs' do it 'a club is created' do expect(@club).to be_present # pass end end describe 'club members' do before do @member = create(:member, club: @club, active_status_id: 1) end context 'a new member' do it 'verifies member associations' do expect(@member.active_status_id).to eq(1) # 注:原代码中approval_status_id应为笔误,修正为active_status_id expect(@member.club).to eq(@club) # pass expect(@club.members.count).to eq(1) # pass expect(@club.members.first).to eq(@member) # pass expect(@club.active_members.count).to eq(1) # pass expect(@club.active_members.first).to eq(@member) # fail # 调试发现:@club.active_members 是 #<ActiveRecord::Associations::CollectionProxy []> end end end end
疑问
为何@club.active_members.count测试通过,但@club.active_members.first测试失败?
问题原因
- 关联缓存与作用域查询的冲突:Rails会缓存已访问的关联集合(比如
@club.members),当访问带作用域的active_members时,默认会尝试从已缓存的members集合中过滤结果,而非重新查询数据库。如果缓存的集合未同步最新的对象状态,就会出现集合为空但数据库计数正确的情况。 autosave: true的同步延迟:has_many关联中设置的autosave: true会触发自动保存逻辑,可能导致内存中的对象状态与数据库数据同步不及时,在测试的事务环境下这个问题会更明显。- 直接设置ID的隐患:Member模型定义了
belongs_to :active_status关联,但测试中直接设置active_status_id: 1而非关联对象,可能导致内存中member对象的关联属性未正确加载,影响Rails对作用域条件的过滤逻辑。
解决方法
方法1:强制刷新关联缓存
在访问active_members前,显式刷新关联缓存,让Rails重新查询数据库:
# 刷新active_members关联 @club.active_members.reload expect(@club.active_members.first).to eq(@member) # 或者刷新整个members关联 @club.members.reload expect(@club.active_members.first).to eq(@member)
方法2:修正FactoryBot配置,正确关联对象
避免直接设置ID,通过关联对象赋值确保内存与数据库状态一致:
# 先定义active_status的工厂 FactoryBot.define do factory :active_status do sequence(:name) { |n| "Active Status #{n}" } # 若需要固定ID,可添加: # id { 1 } end end # 修改member工厂,关联active_status FactoryBot.define do factory :member do association :active_status # 默认创建关联的active_status end end # 测试中创建member的代码调整为: @member = create(:member, club: @club, active_status: create(:active_status, id: 1))
方法3:移除不必要的autosave: true
如果业务逻辑不需要自动保存关联对象,可删除该配置,减少缓存同步问题:
class Club < ApplicationRecord has_many :members, inverse_of: :club, dependent: :nullify has_many :active_members, -> { where(active_status_id: [1, 2]) }, inverse_of: :club, dependent: :nullify end
方法4:调整测试顺序,避免提前加载关联
将active_members的测试放在members之前,避免Rails提前缓存members集合:
context 'a new member' do it 'verifies member associations' do expect(@member.active_status_id).to eq(1) expect(@member.club).to eq(@club) # 先测试active_members expect(@club.active_members.count).to eq(1) expect(@club.active_members.first).to eq(@member) # 再测试members expect(@club.members.count).to eq(1) expect(@club.members.first).to eq(@member) end end
内容的提问来源于stack exchange,提问作者Eric Norcross
相关产品推荐
相关产品推荐

