FactoryGirl 4.5.0自定义before save回调实现方案咨询
解决FactoryGirl 4.5.0中全局before save回调的优雅方案
针对你遇到的问题——需要在所有保存DeletedUser工厂实例的方式(包括create、build后手动save等)中触发ID分配逻辑,而FactoryGirl原生的before(:create)仅覆盖部分场景,这里有两种优雅的实现思路:
方案1:利用ActiveRecord模型的before_save回调(推荐)
最直接的方式是把ID分配逻辑放到DeletedUser模型本身的before_save回调中,这样不管是通过FactoryGirl的哪种方式保存,都会触发这个逻辑,完全覆盖你的所有场景:
class DeletedUser < ActiveRecord::Base # 仅在创建新记录时触发(避免更新时重复分配ID) before_save :assign_unused_id, on: :create def restore User.create!(attributes) # 保留原ID,确保恢复正确 end private def assign_unused_id # 生成一个未被User或DeletedUser占用的ID self.id = loop do # 从当前最大ID+1开始尝试,避免浪费ID candidate_id = [User.maximum(:id), DeletedUser.maximum(:id)].compact.max.to_i + 1 break candidate_id unless User.exists?(id: candidate_id) || DeletedUser.exists?(id: candidate_id) end end end
此时你的工厂定义可以保持极简,无需额外回调:
FactoryGirl.register :deleted_user do # 这里只定义DeletedUser的其他属性即可 end
优点:
- 逻辑集中在模型层,符合Rails的约定俗成
- 自动覆盖所有保存场景(
create/build.save/save!等) - 代码简洁,维护成本低
注意:如果这个ID分配逻辑仅用于测试环境,不想污染生产代码,可以把回调放到测试环境的模型补丁中(比如spec/support/deleted_user_patch.rb)。
方案2:在工厂中动态添加模型级回调(无侵入模型)
如果不想修改生产模型,可以在FactoryGirl的after(:build)回调中,给每个生成的DeletedUser实例动态添加before_save回调:
FactoryGirl.register :deleted_user do after(:build) do |deleted_user| # 定义ID分配的私有方法 deleted_user.define_singleton_method(:assign_unused_id) do self.id = loop do candidate_id = [User.maximum(:id), DeletedUser.maximum(:id)].compact.max.to_i + 1 break candidate_id unless User.exists?(id: candidate_id) || DeletedUser.exists?(id: candidate_id) end end # 给当前实例添加before_save回调(仅创建时触发) deleted_user.before_save :assign_unused_id, on: :create end # 定义其他属性... end
原理:
FactoryGirl的after(:build)会在所有实例构建完成后触发(包括create内部的build步骤),所以不管你用create、build还是build_list,生成的实例都会被动态添加回调,之后调用save/save!时就会触发ID分配逻辑。
优点:
- 完全不修改生产模型,逻辑仅存在于测试工厂中
- 同样覆盖所有保存场景
缺点:
- 代码相对复杂一点,需要理解动态方法和回调的添加逻辑
验证所有场景
两种方案都能覆盖你列出的所有持久化方式:
FactoryGirl.create :deleted_userFactoryGirl.create_list(:deleted_user, n)FactoryGirl.build(:deleted_user).saveFactoryGirl.build(:deleted_user).save!FactoryGirl.build_list(:deleted_user, 2).map(&:save)FactoryGirl.build_list(:deleted_user, 2).map(&:save!)
内容的提问来源于stack exchange,提问作者Kamen Kanev
相关产品推荐
相关产品推荐

