为何查询同一条记录时ActiveRecord生成不同Ruby对象?
解决RSpec测试Gem中数据库视图模型命名作用域的断言问题
最近我碰到一个挺头疼的测试问题:用RSpec和FactoryBot测试Gem里的一个Rails模型命名作用域,这个模型是个没有唯一ID列的数据库视图。明明作用域返回的记录是对的,但contain_exactly断言就是不通过,折腾了好一会儿才搞明白原因,分享给大家。
先说说我的测试场景:我创建了三条测试记录,只有一条符合作用域的筛选条件,测试代码大概是这样的:
RSpec.describe GemNamespace::GemModel, type: :model do before(:all) do FactoryBot.create(:gem_model, :trait1) # 对应记录1 FactoryBot.create(:gem_model, :trait2) # 对应记录2 FactoryBot.create(:gem_model, :trait3) # 对应记录3 end let(:included_record) { GemNamespace::GemModel.where(column: <condition value>) } describe 'my_named_scope' do it 'returns only records matching the conditions' do scope_results = GemNamespace::GemModel.my_named_scope expect(scope_results).to contain_exactly(included_record) end end end
测试一直失败,调试发现scope_results里确实只有那一条符合条件的记录,但它和included_record是不同的Ruby对象,导致断言不通过。之前在普通模型上做同类测试都没问题,就这个Gem里的视图模型出问题,原因主要有这几点:
问题根源分析
- 对象相等性判断依赖主键:Rails默认用模型的主键ID来判断两个ActiveRecord对象是否相等,但这个数据库视图没有唯一ID列,Rails找不到主键,就没法正确识别两个查询返回的是同一条记录,自然会把它们当成不同的对象。
included_record是关系对象而非实例:我用where获取的included_record其实是ActiveRecord::Relation对象,不是单个模型实例,contain_exactly是用来匹配实例集合的,这本身就不对。- Gem环境下的模型初始化差异:因为模型定义在Gem里,Rails加载模型的逻辑和普通项目里的模型略有不同,可能导致主键自动识别失效,进一步影响相等性判断。
解决办法
针对这些问题,我试了几个方案都有效,你可以根据自己的情况选:
1. 先修正included_record的获取方式
把where改成find_by或者first,拿到单个模型实例,而不是关系对象:
let(:included_record) { GemNamespace::GemModel.find_by(column: <condition value>) }
2. 为视图模型显式设置主键
如果你的数据库视图有可以作为唯一标识的列(比如某个业务唯一字段),在Gem的模型里显式指定主键:
module GemNamespace class GemModel < ActiveRecord::Base self.table_name = 'your_view_name' self.primary_key = 'unique_identifier_column' # 替换成视图里的唯一列 end end
这样Rails就会用这个列来判断对象是否相等,contain_exactly就能正常工作了。
3. 自定义对象相等规则
如果视图没有单一的唯一列,可以在模型里重写==方法,定义自己的相等判断逻辑:
module GemNamespace class GemModel < ActiveRecord::Base def ==(other) # 用能唯一标识记录的字段组合来判断 other.is_a?(self.class) && other.attributes.slice('col1', 'col2', 'col3') == attributes.slice('col1', 'col2', 'col3') end end end
4. 换一种断言方式
如果不想改模型代码,也可以避开对象相等性的问题,直接检查记录的属性或者数量:
it 'returns only records matching the conditions' do scope_results = GemNamespace::GemModel.my_named_scope expect(scope_results.count).to eq(1) expect(scope_results.first.column).to eq(<condition value>) end
或者直接检查字段值的集合:
expect(scope_results.pluck(:column)).to contain_exactly(<condition value>)
内容的提问来源于stack exchange,提问作者istrasci
相关产品推荐
相关产品推荐

