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

Rails使用RSpec mock单例类方法完成控制器测试方案

RSpec Mock单例模块类方法的正确实现

问题根因

报错核心原因是mock目标选择错误:

  • find_by_id是定义在class << self块内的模块单例方法(类方法),调用形式为EstablishmentsQueryService.find_by_id(Ruby生态里用.前缀标记类/单例方法)。
  • allow_any_instance_of仅用于给模块/类的实例方法打桩,对应方法用#前缀标记,和你要mock的单例方法完全是两个独立方法,所以RSpec会抛出EstablishmentsQueryService does not implement #find_by_id错误,mock配置完全不生效,代码会直接执行真实数据库查询逻辑。
  • 你在PUT用例里直接allow(EstablishmentsQueryService)的方向是正确的,但将allow对象拆分存到let中再链式调用容易出现上下文失效问题,加上代码中存在的笔误、请求缺参问题,最终导致测试一直无法通过。

可直接复用的正确Mock写法

1. GET场景修复(存在对应记录)

直接对模块本身的单例方法打桩即可,不需要使用any_instance相关方法,同时修正原有代码的笔误:

context 'when has establishment' do
  let(:establishment_id) { SecureRandom.uuid }
  let(:establishment) { build(:establishment, id: establishment_id, created_at: DateTime.now, updated_at: DateTime.now) }

  before do
    # 核心:直接mock模块的单例方法,可加with参数校验入参
    allow(EstablishmentsQueryService).to receive(:find_by_id)
      .with(establishment_id)
      .and_return(establishment)
    get "/api/v1/establishments/#{establishment_id}"
  end

  it 'then http status is ok' do
    expect_status_is_ok
  end

  it 'has body equal to expected' do
    expect_field_by_field(establishment, body_to_open_struct, %i[id name cnpj description])
  end
end

2. GET场景修复(记录不存在)

不需要真的查库触发异常,直接让mock方法抛出业务定义的异常即可:

context 'when has no establishment' do
  let(:non_exist_id) { SecureRandom.uuid }
  before do
    allow(EstablishmentsQueryService).to receive(:find_by_id)
      .with(non_exist_id)
      .and_raise(EstablishmentNotFoundError.new(non_exist_id))
    get "/api/v1/establishments/#{non_exist_id}"
  end

  it 'then http status is not_found' do
    expect_status_is_not_found
  end
end

说明:参数非法(非UUID格式)的场景不需要mock query service,因为validate_id的before_action会在load_establishment之前执行,直接返回400,原有代码逻辑可正常运行。

3. PUT场景修复

不要把allow对象拆分存到let中,直接在before块内完成打桩,同时补全缺失的请求参数、修正笔误:

context 'when updated with success' do
  let(:establishment) { build(:establishment) }
  let(:target_id) { SecureRandom.uuid }
  let(:mock_loaded_establishment) { build(:establishment, id: target_id, created_at: DateTime.now, updated_at: DateTime.now) }

  before do
    # Mock查询服务
    allow(EstablishmentsQueryService).to receive(:find_by_id)
      .with(target_id)
      .and_return(mock_loaded_establishment)
    # Mock保存服务
    allow(EstablishmentService).to receive(:save) do |record|
      record.updated_at = DateTime.now
      record
    end
    # 补全PUT请求必填的establishment参数
    put "/api/v1/establishments/#{target_id}", params: {
      establishment: {
        name: establishment.name,
        cnpj: establishment.cnpj,
        description: establishment.description
      }
    }
  end

  it 'then http status is ok' do
    expect_status_is_ok
  end

  it 'has body equal to expected' do
    actual = body_to_open_struct
    expect_field_by_field(establishment, actual, %i[name cnpj description])
    expect(actual.id).to eq(target_id)
  end
end

通用注意事项

  • 所有定义在class << self块内、或者用def self.xxx形式定义的方法,都属于类/模块的单例方法,mock时直接使用allow(类名/模块名).to receive(方法名)即可,禁止用allow_any_instance_of mock这类方法。
  • 原有测试代码存在几处会导致失败的笔误:id: p中的p是未定义变量、except_field_by_field应为expect_field_by_field、expected(actual.id)应为expect(actual.id)、PUT/POST请求未传必填的establishment参数,需要一并修正。
  • 如果需要校验控制器确实调用了对应服务,可以在测试中增加断言:expect(EstablishmentsQueryService).to have_received(:find_by_id).with(target_id),避免逻辑走偏。

内容的提问来源于stack exchange,提问作者Jose Luiz Junior

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 17:36:24