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

如何用Minitest测试processAConsumer中begin-rescue块的重试逻辑?

问题描述

我们有一个幂等服务ProcessA,调用后会查找或创建Payment记录并返回:

class ProcessA
  attr_reader :payment

  def initialize(operation_id)
    @payment = Payment.find_or_initialize_by(operation_id: operation_id)
  end

  def call
    update_payment_attributes
    payment.save!
  end

  private

  def update_payment_attributes
    # 赋值/更新支付记录属性
  end
end

由于幂等性设计,ProcessA会先查找匹配operation_id的现有记录,找不到就实例化并保存新记录。但如果创建新记录时和ProcessB发生竞态,ProcessB先创建了同operation_id的Payment,ProcessA保存时会触发模型约束,抛出ActiveRecord::RecordNotUnique错误。

为此我在ProcessAConsumer服务中添加了重试逻辑,当捕获到指定错误时会重试调用,以获取ProcessB创建的Payment:

class ProcessAConsumer
  
  def call(operation_id)
    begin
      retries ||= 0
      payment = ProcessA.new(operation_id).call
    rescue PG::UniqueViolation, ActiveRecord::RecordNotUnique
      retry if (retries += 1) < 3
    end
  end
end

现在需要用Minitest测试这个重试逻辑,我想StubProcessA的首次#call方法让它抛出错误来触发重试,但尝试的代码没成功:

test "retry logic should succeed" do
    ProcessA.any_instance.expects(:call) do
      1.times do
        raise PG::UniqueViolation, ActiveRecord::RecordNotUnique
      end
    end

    response = ProcessAConsumer.call(123)

    assert response.success? # 失败,因为重试逻辑没触发
end
解决方案

你的测试代码问题在于,any_instance.expects(:call)的写法无法控制「首次调用抛错、后续调用正常返回」的逻辑。可以用以下两种方式实现正确的测试:

方式1:用Stub结合计数器控制行为

test "retry logic handles unique violation and succeeds on retry" do
  retry_count = 0

  ProcessA.stub :new, lambda { |operation_id|
    instance = ProcessA.new(operation_id)
    # 重写实例的call方法
    def instance.call(retry_count)
      if retry_count < 1
        retry_count[0] += 1
        raise ActiveRecord::RecordNotUnique
      else
        @payment = Payment.new(operation_id: operation_id, status: :success)
        @payment
      end
    end
    # 用数组传递计数器,避免闭包变量不可修改的问题
    instance.define_singleton_method(:call) { instance.call([retry_count]) }
    instance
  } do
    result = ProcessAConsumer.new.call(123)
    assert_instance_of Payment, result
    assert_equal :success, result.status
    assert_equal 1, retry_count
  end
end

方式2:用Minitest-Mock灵活控制调用行为

test "retry logic triggers on unique violation" do
  mock_process_a = Minitest::Mock.new
  # 定义两次调用的行为:第一次抛错,第二次返回正常Payment
  mock_process_a.expect :call, -> { raise ActiveRecord::RecordNotUnique }, []
  mock_process_a.expect :call, Payment.new(operation_id: 123, status: :success), []

  # 让ProcessA.new返回这个mock实例
  ProcessA.stub :new, ->(operation_id) { mock_process_a } do
    result = ProcessAConsumer.new.call(123)
    assert_instance_of Payment, result
    # 验证mock的调用期望都被满足
    mock_process_a.verify
  end
end

关键说明

  • 原测试代码中,1.times do raise...的写法只会触发一次错误,但any_instance.expects(:call)默认只期望被调用一次,重试触发第二次调用时会因超出期望次数失败。
  • 必须明确控制call方法的多次调用行为:第一次抛错,后续调用正常返回,才能触发重试逻辑并验证最终成功。
  • 注意Ruby类名规范:类名应使用大驼峰格式(如ProcessA而非processA),符合社区通用规范。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 23:58:25