如何为TimeoutError异常编写RSpec测试用例
邮件发送重试逻辑RSpec测试用例
以下测试用例覆盖全部逻辑分支,测试中stub掉真实sleep避免单测执行耗时过长:
# 注意将描述类名替换为你项目中实际定义send_mail方法的模块/类名 require 'rails_helper' RSpec.describe MailDeliveryService, type: :service do let(:mock_mail) { double('ActionMailer::Message') } let(:mock_log_obj) { double('BusinessLog') } let(:test_conn_refused) { Errno::ECONNREFUSED.new } before do allow(subject).to receive(:sleep) allow(CommonLogger).to receive(:input_log) allow(Rails.logger).to receive(:info) allow(Rails.logger).to receive(:error) end describe '#send_mail' do context '邮件首次发送成功' do it '不触发任何重试逻辑,无等待、无重试日志' do expect(mock_mail).to receive(:deliver_now).once expect(subject).not_to receive(:sleep) expect(Rails.logger).not_to receive(:info).with('Retrying...') subject.send_mail(mock_mail, mock_log_obj) end end context '发送命中可重试异常类型' do before do allow(mock_mail).to receive(:deliver_now).and_raise(test_conn_refused) end it '累计最多发起4次发送尝试,每次重试前等待1秒' do expect { subject.send_mail(mock_mail, mock_log_obj) }.to raise_error(test_conn_refused) expect(mock_mail).to have_received(:deliver_now).exactly(4).times # 前3次失败后各等待1秒,第4次失败直接抛错无等待 expect(subject).to have_received(:sleep).with(1).exactly(3).times end it '每次失败都写入对应次数的异常日志,重试时输出info级别日志' do expect { subject.send_mail(mock_mail, mock_log_obj) }.to raise_error(test_conn_refused) (1..3).each do |attempt| expect(CommonLogger).to have_received(:input_log).with( mock_log_obj, "Timeout: #{test_conn_refused} (attempt ##{attempt})" ) end expect(Rails.logger).to have_received(:info).with('Retrying...').exactly(3).times end it '4次尝试全部失败后输出重试超限日志,抛出原始异常' do expect { subject.send_mail(mock_mail, mock_log_obj) }.to raise_error(Errno::ECONNREFUSED) expect(Rails.logger).to have_received(:error).with('Retry attempts exceeded.').once end it '重试过程中任意一次发送成功即终止后续重试' do attempt_count = 0 allow(mock_mail).to receive(:deliver_now) do attempt_count += 1 raise test_conn_refused if attempt_count < 3 end expect { subject.send_mail(mock_mail, mock_log_obj) }.not_to raise_error expect(mock_mail).to have_received(:deliver_now).exactly(3).times expect(subject).to have_received(:sleep).with(1).exactly(2).times expect(Rails.logger).not_to have_received(:error).with('Retry attempts exceeded.') end end context '发送抛出非捕获范围的异常' do let(:unexpected_error) { StandardError.new('unexpected mail delivery error') } it '不触发重试,直接抛出原异常' do allow(mock_mail).to receive(:deliver_now).and_raise(unexpected_error) expect { subject.send_mail(mock_mail, mock_log_obj) }.to raise_error(unexpected_error) expect(mock_mail).to have_received(:deliver_now).once expect(subject).not_to receive(:sleep) end end # 遍历所有声明捕获的异常类型,确保异常匹配逻辑无遗漏 [Errno::ECONNREFUSED, Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout].each do |ex_klass| context "触发#{ex_klass.name}类型异常" do let(:target_ex) { ex_klass.new } it '正常进入重试流程' do allow(mock_mail).to receive(:deliver_now).and_raise(target_ex) expect { subject.send_mail(mock_mail, mock_log_obj) }.to raise_error(target_ex) expect(mock_mail).to have_received(:deliver_now).exactly(4).times end end end end end
测试覆盖点说明
- 首次发送成功的正常分支
- 可重试异常下的尝试次数、重试间隔校验
- 重试过程中恢复成功的分支
- 4次尝试全部超限后的日志输出、异常抛出逻辑
- 非捕获异常直接透传不重试的分支
- 4类声明捕获的网络异常的匹配逻辑校验
内容的提问来源于stack exchange,提问作者Rashmi Balkur
相关产品推荐
相关产品推荐

