You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 00:23:10