Rails 4.2.3中如何在FactoryGirl工厂内正确模拟模型回调
环境为Rails 4.2.3,User模型注册了after_create回调,创建用户后会调用发布逻辑:
after_create :publish # ... def publish Mymodule::Publisher.new_user(user: self, x_forward: {}) end
最初尝试在FactoryGirl工厂定义中直接stub该回调,避免测试时触发真实发布逻辑,写法如下:
FactoryGirl.define do # ... factory :user do after(:build) do |user| allow(user).to receive(:publish) end end end
运行所有调用create(:user)的测试时抛出错误:
The use of doubles or partial doubles from rspec-mocks outside of the per-test lifecycle is not supported.
报错核心原因是FactoryGirl的定义代码在测试套件启动阶段就会加载执行,不属于rspec每个测试用例的独立生命周期,rspec-mocks不允许在这个阶段创建mock/stub对象。
以下三种方案都可以实现需求,根据测试场景选择即可:
方案1:通过trait临时跳过ActiveRecord回调
不依赖rspec-mock,直接利用ActiveRecord自带的回调开关实现跳过逻辑,封装为trait按需调用:
FactoryGirl.define do factory :user do # 基础工厂保留原逻辑,和生产环境行为一致 trait :skip_publish do after(:build) do User.skip_callback(:create, :after, :publish) end end end end
测试中不需要触发发布逻辑时,直接调用create(:user, :skip_publish)即可。如果需要避免回调跳过影响其他测试用例,可以在测试的teardown阶段重新注册回调:
after do User.set_callback(:create, :after, :publish) end
方案2:直接在工厂内重写实例方法
不需要引入mock框架能力,在实例构建阶段直接给当前user对象重写空的publish方法,覆盖原类的方法实现:
FactoryGirl.define do factory :user do after(:build) do |user| def user.publish # 空实现,阻断原发布逻辑 true end end end end
这种写法完全不涉及rspec-mocks的调用,不会触发生命周期校验错误,所有通过工厂生成的user实例都不会执行真实的发布逻辑。如果需要部分场景保留原回调,把这段逻辑放到单独的trait里按需引用即可。
方案3:mock回调内部的依赖对象
如果不希望直接跳过回调,同时需要验证回调确实被正常触发,可以不stub回调本身,而是stub回调内部调用的Mymodule::Publisher类,阻断真实外部调用:
FactoryGirl.define do factory :user do after(:build) do allow(Mymodule::Publisher).to receive(:new_user).and_return(true) end end end
这种写法需要确保rspec配置中允许在工厂加载阶段注入mock,一般在spec/rails_helper.rb中配置如下规则即可正常运行:
RSpec.configure do |config| config.before(:suite) do FactoryGirl.find_definitions end end
相比直接跳过回调,这种方式可以保证回调链路本身是正常执行的,只是阻断了外部IO调用,更贴近真实代码行为。
内容的提问来源于stack exchange,提问作者Dave

