2. 异常与重试基础

在自动化测试中,脚本执行失败几乎是不可避免的。网络抖动、服务短暂不可用、资源竞争、第三方接口超时,都会让原本正确的测试逻辑在某一刻失败。要构建稳定的测试体系,第一步不是急着加 try/except,而是先理解异常是什么、它如何产生、如何传播、何时应该被处理,再理解重试解决的是哪一类问题。异常与重试是两套机制:异常负责“发现并描述问题”,重试负责“对可恢复问题再给一次机会”。

一、异常的定义与本质

异常(Exception)是程序在执行过程中遇到的、打断正常指令流的特殊事件。它通常包含三部分信息:类型(发生了什么)、消息(具体描述)、调用栈(在哪里发生、经过哪些调用路径)。在测试脚本中,异常不仅是错误信号,更是定位问题的关键证据。例如断言失败、元素找不到、连接超时,虽然都表现为“脚本挂了”,但它们的类型和恢复策略完全不同。

异常 ≠ 失败。异常是运行时抛出的对象;失败是测试用例的最终判定结果。一个异常可能被捕获并恢复,最终用例仍然通过;也可能未被处理,导致用例失败。

二、异常的分类

从测试脚本的恢复视角,异常可以按“是否可恢复”和“来源”两个维度分类。按可恢复性分为:瞬时异常(网络抖动、限流、临时超时,重试可能成功)、持久异常(配置错误、代码缺陷、数据缺失,重试必然再次失败)、致命异常(进程崩溃、内存溢出,需要终止并上报)。按来源分为:环境异常(网络、DNS、端口)、应用异常(服务返回 5xx、业务错误码)、脚本异常(选择器错误、语法错误、断言不匹配)、数据异常(测试数据被污染、依赖数据未就绪)。

异常类型典型示例是否可重试推荐处理方式
瞬时环境异常ConnectionTimeout、502/503是有限次退避重试
限流异常429 Too Many Requests是按 Retry-After 延迟重试
脚本异常NoSuchElement、语法错误否修复脚本,不重试
断言失败期望值与实际值不符否记录证据,判定失败
数据异常依赖数据未生成视情况等待或重建数据后重试
致命异常进程崩溃、OOM否终止并告警

三、异常的生命周期

异常从产生到消亡,通常经历五个阶段:抛出(raise)→ 传播(propagate)→ 捕获(catch)→ 处理(handle)→ 终结(finalize)。抛出时,运行时创建异常对象并中断当前执行;传播时,异常沿调用栈向上寻找匹配的处理器;捕获时,最近的 except 或 catch 块接管控制权;处理时,可以记录日志、清理资源、决定重试或重新抛出;终结时,无论是否处理,finally 块都会执行,用于释放连接、关闭文件等。

异常生命周期与重试决策 抛出 raise 创建异常对象 传播 propagate 沿调用栈上溯 捕获 catch 匹配处理器 处理 handle 日志/清理/决策 终结 finally 释放资源 判断:异常是否可恢复? 瞬时 / 限流 / 依赖未就绪 进入重试流程 退避 + 上限 + 幂等校验 直接失败并上报 保留堆栈与现场证据 是 否 记录异常类型与堆栈

理解生命周期的意义在于:重试应该发生在“处理”阶段,而不是“捕获”阶段。捕获只是拿到控制权,处理才决定是重试、降级还是失败。很多脚本把重试逻辑直接塞进 except 里,导致异常被吞掉、堆栈丢失,最终无法定位根因。

四、重试的基本概念

重试(Retry)是指当一次操作因可恢复原因失败后,在满足约束条件的前提下再次执行同一操作。一个完整的重试策略通常包含五个要素:重试条件(哪些异常才重试)、最大次数(避免无限循环)、退避策略(固定间隔、指数退避、随机抖动)、超时上限(总耗时不能超过用例预算)、幂等性保证(重复执行不会产生副作用)。

retry_policy:
  max_attempts: 3
  backoff: exponential
  base_delay: 0.5s
  max_delay: 5s
  jitter: true
  retry_on:
    - ConnectionTimeout
    - HTTP 502
    - HTTP 503
    - HTTP 429
  no_retry_on:
    - AssertionError
    - NoSuchElementException
    - HTTP 400

五、适用场景与常见误区

重试适合处理瞬时性、外部性、可恢复的问题:网络请求超时、服务短暂不可用、限流、消息队列暂时为空、分布式锁竞争。它不适合处理确定性、逻辑性、数据性的问题:断言失败、选择器写错、参数错误、业务规则不满足。把不该重试的异常拿去重试,只会延长失败时间、掩盖真实缺陷。

常见误区一:无差别重试所有异常。这会让脚本缺陷被“重试成功”掩盖,问题被推迟到更晚才暴露。
常见误区二:重试次数过多或间隔过短。在服务已经过载时继续高频重试,会加剧雪崩。
常见误区三:忽略幂等性。对“创建订单”“扣款”等非幂等操作重试,可能产生重复数据。
常见误区四:重试后不记录原始异常。最终只看到最后一次失败,丢失了首次失败的关键现场。
常见误区五:把重试当作修复手段。重试只能缓解瞬时问题,不能替代根因修复。

六、案例:一次接口超时的正确处理

假设测试脚本调用订单查询接口,返回 ConnectionTimeout。正确做法是:捕获该异常后,判断它属于瞬时环境异常,进入重试流程;每次重试前记录尝试次数和耗时;采用指数退避,例如 0.5s、1s、2s;最多 3 次;若仍失败,则抛出带有完整尝试记录的异常,标记用例失败并附上日志。若接口返回的是 400 Bad Request,则说明请求本身有问题,应立即失败,不进入重试。

判断口诀:“外部瞬时可重试,内部确定不重试;重试必须有限度,幂等记录不能丢。” 把异常分类做对,重试策略才有意义;把生命周期看清,异常处理才不会吞掉真相。

异常与重试是测试脚本稳定性的两块基石。异常负责准确描述“出了什么问题”,重试负责对“可恢复的问题”进行有限补救。只有先分清异常类型、看清传播路径、守住幂等与上限,重试才不会变成掩盖缺陷的“遮羞布”,而成为提升测试稳定性的有效手段。