无需额外Gem,如何用RSpec测试报错重试逻辑?
问题分析与解决
代码与测试重现
业务代码
def make_error raise StandardError, 'err' end class Bob def bar(thing) puts thing end end def foo ['a', 'b'].each do |x| retry_counter = 5 begin bar(x) make_error rescue StandardError => e if retry_counter.positive? retry_counter -= 1 retry else raise e end end end end
RSpec测试代码
it 'adds and retries the emails to the digest store' do expect(Bob).to receive(:bar).exactly(6).times.with('a') expect{ foo }.to raise_error(StandardError) end
核心问题诊断
方法调用不匹配:测试期望
Bob类的bar类方法被调用,但业务代码里的bar(x)是在顶级作用域调用未定义的方法,实际运行会抛出NoMethodError,而非预期的StandardError。即便rescue能捕获该错误,测试期望与实际调用对象的不匹配也会导致断言逻辑混乱。RSpec桩行为干扰:默认情况下,
expect(Bob).to receive(:bar)会为Bob类创建桩方法,但业务代码根本没调用这个桩方法,导致:- 保留第一个断言时,代码因
bar(x)未定义抛出错误,重试逻辑虽执行,但RSpec最终因期望未满足失败,且错误类型与测试预期不符。 - 移除第一个断言时,代码执行到
make_error抛出StandardError,断言成功,但无法验证重试次数。
- 保留第一个断言时,代码因
解决方案
步骤1:修正方法调用与定义
将Bob的bar改为类方法,并在foo中明确调用:
class Bob def self.bar(thing) # 改为类方法 puts thing end end def foo ['a', 'b'].each do |x| retry_counter = 5 begin Bob.bar(x) # 明确调用类方法 make_error rescue StandardError => e if retry_counter.positive? retry_counter -= 1 retry else raise e end end end end
步骤2:调整RSpec断言逻辑
使用and_call_original确保桩方法执行原逻辑,同时保留断言验证:
it 'adds and retries the emails to the digest store' do expect(Bob).to receive(:bar).exactly(6).times.with('a').and_call_original expect { foo }.to raise_error(StandardError) end
为什么这样能解决问题?
- 修正后的
Bob.bar是类方法,与测试期望的调用对象一致,确保重试逻辑中每次调用都能触发断言计数。 and_call_original让RSpec在验证调用次数的同时,执行原方法的puts逻辑,不影响后续make_error的错误抛出。- 重试逻辑中
retry_counter的作用域正确,会重试5次,总共调用6次Bob.bar('a'),随后抛出StandardError,两个断言均可通过。
内容的提问来源于stack exchange,提问作者mangocaptain
相关产品推荐
相关产品推荐

