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

如何为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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 03:45:33