如何在RSpec中存根Rails里的父类与嵌套子类
问题场景
我在Rails 7应用中有如下类结构:
代码结构
app/services/email_notifier.rb
class EmailNotifier def notify(template_data) # do things with email client end end
app/services/email_notifier/template_data.rb
class EmailNotifier class TemplateData def fill_in # do things with template and email client end end end
app/services/notification_service.rb
class NotificationService def call template_content = EmailNotifier::TemplateData.new(name: 'John Doe') EmailNotifier.notify(template_content) end end
测试代码
为验证类方法调用,我编写了如下RSpec测试:
# spec/services/notification_service.rb require 'rails_helper' RSpec.describe NotificationService do subject(:service) { described_class.new } let(:email_notifier) { class_double(EmailNotifier).as_stubbed_const } let(:email_template_class) { class_double(EmailNotifier::TemplateData).as_stubbed_const } let(:email_template) { instance_double(EmailNotifier::TemplateData) } before do allow(email_notifier).to receive(:notify) allow(email_template_class).to receive(:new).and_return(:email_template) allow(email_template).to receive(:fill_in) end it 'fills the template in' do service.call expect(email_template).to have_received(:fill_in) end end
错误信息
运行测试时抛出如下错误:
NameError: uninitialized constant #<ClassDouble(EmailNotifier) (anonymous)>::TemplateData template_content = EmailNotifier::TemplateData.new
我怀疑问题出在父类被存根后无法获取嵌套子类,想请教如何正确实现这类存根?我的处理方式是否有误?stub_const的行为是否符合预期?
解决方案
你的判断正确,问题根源是用class_double(EmailNotifier).as_stubbed_const替换了EmailNotifier类后,原嵌套类EmailNotifier::TemplateData不再关联到这个存根类上。以下是几种可行的修复方案:
方案1:调整存根顺序,先存根嵌套类
Rails自动加载机制下,先存根嵌套类再存根父类,嵌套类会自动关联到存根的父类。修改测试的let定义顺序:
RSpec.describe NotificationService do subject(:service) { described_class.new } # 先定义嵌套类的存根 let(:email_template_class) { class_double(EmailNotifier::TemplateData).as_stubbed_const } # 再定义父类的存根 let(:email_notifier) { class_double(EmailNotifier).as_stubbed_const } let(:email_template) { instance_double(EmailNotifier::TemplateData) } before do allow(email_notifier).to receive(:notify) # 注意:此处需返回实例double变量,而非符号:email_template allow(email_template_class).to receive(:new).and_return(email_template) allow(email_template).to receive(:fill_in) end it 'fills the template in' do service.call expect(email_template).to have_received(:fill_in) end end
另外注意原测试中and_return(:email_template)是错误的,需返回定义好的email_template实例double,否则无法捕获fill_in方法的调用。
方案2:手动挂载嵌套类存根到父类存根
如果需要保持存根顺序,可以手动将嵌套类存根赋值给存根父类的常量:
RSpec.describe NotificationService do subject(:service) { described_class.new } let(:email_notifier) { class_double(EmailNotifier).as_stubbed_const } let(:email_template_class) { class_double(EmailNotifier::TemplateData) } let(:email_template) { instance_double(EmailNotifier::TemplateData) } before do # 手动将嵌套类存根挂载到存根的父类上 email_notifier.const_set(:TemplateData, email_template_class) allow(email_notifier).to receive(:notify) allow(email_template_class).to receive(:new).and_return(email_template) allow(email_template).to receive(:fill_in) end it 'fills the template in' do service.call expect(email_template).to have_received(:fill_in) end end
这里去掉了email_template_class的as_stubbed_const,改为手动挂载到存根的EmailNotifier类上。
方案3:仅存根需要的方法,不替换整个类
如果不需要存根整个EmailNotifier类,仅需验证notify方法调用,可直接存根目标方法,避免替换整个类带来的嵌套类关联问题,这也是更轻量、推荐的做法:
RSpec.describe NotificationService do subject(:service) { described_class.new } let(:email_template) { instance_double(EmailNotifier::TemplateData) } before do # 只存根EmailNotifier的notify方法 allow(EmailNotifier).to receive(:notify) # 精准存根TemplateData的new方法,匹配参数 allow(EmailNotifier::TemplateData).to receive(:new).with(name: 'John Doe').and_return(email_template) allow(email_template).to receive(:fill_in) end it 'fills the template in' do service.call expect(email_template).to have_received(:fill_in) end it 'calls notify with the template' do service.call expect(EmailNotifier).to have_received(:notify).with(email_template) end end
内容的提问来源于stack exchange,提问作者Pipetus
相关产品推荐
相关产品推荐

