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

Rspec存根Rails凭证触发NilClass错误的技术求助

问题分析

错误的核心原因是类常量在Ruby类加载阶段就会被初始化,而你所有的RSpec before 块都是在类加载完成后才执行的。也就是说,当RSpec加载 Vendor::Connection 类时,VENDOR_CLIENT_ID 和 VENDOR_CLIENT_SECRET 就已经尝试读取 Rails.application.credentials.vendor_api 了,此时你的模拟逻辑还没运行,导致 vendor_api 返回nil,进而触发 undefined method [] for nil:NilClass 错误。

解决方案

以下是几种可行的解决方式,按推荐程度排序:

方案1:在类加载前模拟Credentials

通过提前模拟credentials,确保在 Vendor::Connection 类加载前就把依赖的凭证数据准备好:

require 'rails_helper'

# 在加载Vendor::Connection类之前先模拟凭证
allow(Rails.application).to receive(:credentials).and_return(
  OpenStruct.new(vendor_api: { client_id: '123', client_secret: '456' })
)

describe Vendor::Connection do
  it '基础测试通过' do
    expect(true).to eq(true)
  end

  it '生成正确的token请求体' do
    expect(described_class.new.token_body).to eq(
      { client_id: '123', client_secret: '456' }
    )
  end
end

如果你的类在 rails_helper 中被预加载,上述代码的位置必须放在 require 'rails_helper' 之后、describe 之前,确保优先级高于类加载。

方案2:重构代码,延迟常量初始化

把直接初始化的常量改成懒加载的类方法,让凭证读取逻辑延迟到实际调用时执行:

class Vendor::Connection
  def self.vendor_client_id
    @vendor_client_id ||= Rails.application.credentials.vendor_api[:client_id].freeze
  end

  def self.vendor_client_secret
    @vendor_client_secret ||= Rails.application.credentials.vendor_api[:client_secret].freeze
  end

  def token_body
    {
      client_id: self.class.vendor_client_id,
      client_secret: self.class.vendor_client_secret,
    }
  end
end

此时测试可以正常使用 before 块模拟凭证:

require 'rails_helper'

describe Vendor::Connection do
  before do
    allow(Rails.application.credentials).to receive(:vendor_api).and_return(
      { client_id: '123', client_secret: '456' }
    )
  end

  it '生成正确的token请求体' do
    expect(described_class.new.token_body).to eq(
      { client_id: '123', client_secret: '456' }
    )
  end
end

方案3:卸载类后重新加载并Stub常量

如果不想重构原有代码,可以先卸载已加载的类,Stub常量后再重新加载类文件:

require 'rails_helper'

describe Vendor::Connection do
  before do
    # 卸载已加载的类
    Object.send(:remove_const, 'Vendor::Connection')
    # Stub目标常量
    stub_const('Vendor::Connection::VENDOR_CLIENT_ID', '123')
    stub_const('Vendor::Connection::VENDOR_CLIENT_SECRET', '456')
    # 重新加载类文件(替换为你的类实际路径)
    load 'vendor/connection.rb'
  end

  it '常量值符合预期' do
    expect(described_class::VENDOR_CLIENT_ID).to eq('123')
  end
end

这种方式需要注意类文件路径的正确性,且可能影响其他测试用例,建议仅在单个测试文件中使用。

关键提示
  • Ruby的类常量是加载时求值,所有依赖外部资源(如凭证、环境变量)的常量初始化,都可能在测试中遇到类似问题。
  • 优先选择方案2的重构方式,让代码依赖更清晰,也更易于测试和维护。

内容的提问来源于stack exchange,提问作者Paul Pettengill

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 16:20:08