如何用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
相关产品推荐
相关产品推荐

