本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介: pytest-rerunfailures 是一个增强 pytest 测试框架功能的实用插件,支持自动重跑失败的测试用例,有效应对CI环境中因网络波动、资源竞争等临时性问题导致的测试失败。通过命令行参数或配置文件可灵活设置重试次数和策略,提升测试稳定性与开发效率。本文介绍该插件的安装、配置及与其他常用插件(如pytest-xdist、pytest-cov)协同使用的最佳实践,帮助构建高效可靠的自动化测试体系。

pytest-rerunfailures:让自动化测试不再“一击即溃” 💥

你有没有经历过这样的场景?CI 构建莫名其妙地失败了,点开日志一看——某个 API 测试用例因为 ConnectionError 挂掉了。重新跑一遍,又绿了 🟢。再过两天,同样的问题再次上演。

这种情况在现代软件开发中太常见了。我们构建的系统越来越复杂,依赖越来越多:数据库、缓存、第三方服务、消息队列……这些外部组件哪怕只是短暂抖动一下,都可能让原本健康的测试用例“躺枪”。

这时候你会想:“这根本不是代码的问题啊!”
但 CI 可不管这些,它只会冷冷地告诉你: 构建失败 ❌。

于是,团队成员开始频繁地手动重试构建,信任感逐渐崩塌,流水线的健康度也每况愈下。长此以往,大家甚至会对红色状态习以为常,“破窗效应”悄然蔓延。

“反正每次都能重试成功,这次也一定没问题。”
——直到某天,真正的 bug 被这种惯性忽略……

为了解决这个痛点, pytest-rerunfailures 应运而生。它不是一个掩盖缺陷的“遮羞布”,而是一把精准的手术刀——专门切除那些由环境噪声引起的“伪失败”,让真正需要关注的问题浮出水面。


安装与配置:从零到一,只需三步 🛠️

第一步:装上插件,就像装个“保险丝”

Python 的生态强大之处就在于它的包管理工具 pip 。安装 pytest-rerunfailures 简直不能再简单:

pip install pytest-rerunfailures

一句话搞定!🎉
这条命令会自动帮你处理所有依赖关系,包括兼容版本的 pytest 核心库(要求至少 pytest>=5.0 )。如果你还没装 pytest ,别担心, pip 会顺手给你补上。

但在真实项目中,我们追求的是 可复现性 和 一致性 。所以更推荐的做法是把依赖写进 requirements.txt :

pytest==7.4.0
pytest-rerunfailures==12.3

然后统一安装:

pip install -r requirements.txt

这样无论是本地开发、CI 构建还是同事拉代码,环境都是一致的,不会出现“在我机器上明明好好的”这类经典甩锅语录 😅。


第二步:验证是否真的“上岗”了 ✅

装完不能光看输出信息就信以为真,得确认插件已经被 Pytest 正确加载才行。

运行以下命令看看帮助文档里有没有我们的新朋友:

pytest --help

如果一切顺利,在输出末尾你应该能看到这两行:

  --reruns=num         Number of times to retry failed tests.
  --reruns-delay=secs  Delay between test retries in seconds.

这就说明 pytest-rerunfailures 已经成功注册!

为了进一步确认,还可以检查版本号:

pytest --version

输出类似:

This is pytest version 7.4.0, with plugins:
  pytest-rerunfailures-12.3

完美!👏

不过这里有个小坑需要注意:不同版本之间存在兼容性问题。比如:

  • pytest-rerunfailures >= 12.0 需要 pytest >= 7.0
  • 如果你在用 pytest-xdist 做并行测试,建议搭配 rerunfailures >= 11.0
  • 老项目还在用 pytest==6.x ?那就要锁定 pytest-rerunfailures==10.*

为了避免踩雷,可以加一段脚本做前置校验:

import pytest
import pkg_resources

def check_plugin_installed():
    try:
        dist = pkg_resources.get_distribution("pytest-rerunfailures")
        print(f"✅ pytest-rerunfailures {dist.version} installed.")
    except pkg_resources.DistributionNotFound:
        print("❌ pytest-rerunfailures not found.")
        exit(1)

    # 自动判断所需最低版本
    if pytest.__version__.startswith("7"):
        required_version = "12.0"
    else:
        required_version = "10.0"

    current = tuple(map(int, dist.version.split('.')))
    required = tuple(map(int, required_version.split('.')))

    if current < required:
        print(f"⚠️ Version too low. Require >= {required_version}")
        exit(1)

这段代码可以在 CI 流程早期执行,防止因版本错配导致重试机制失效,属于典型的“防呆设计”。


第三步:排查常见安装问题 🔍

虽然安装过程通常很顺畅,但在某些受限环境下还是会遇到麻烦。下面这几个错误你很可能已经见过👇:

❌ 找不到包?网络被墙了!
ERROR: Could not find a version that satisfies the requirement pytest-rerunfailures

这是典型的企业内网限制访问 PyPI 导致的。解决方案很简单——换国内镜像源!

pip install pytest-rerunfailures -i https://mirrors.aliyun.com/pypi/simple/

或者更优雅一点,配置全局镜像:

# Linux: ~/.pip/pip.conf
# Windows: %APPDATA%\pip\pip.ini

[global]
index-url = https://mirrors.aliyun.com/pypi/simple/
trusted-host = mirrors.aliyun.com

从此告别超时烦恼。


❌ 权限不足?别往系统目录硬塞!
PermissionError: [Errno 13] Permission denied: '/usr/local/lib/python3.9/site-packages/...'

这是你在共享服务器上常见的权限问题。强行用 sudo 不是好主意,容易污染系统环境。

推荐两种安全做法:

  1. 用户级安装 :
    bash pip install --user pytest-rerunfailures

  2. 虚拟环境隔离 (强烈推荐):
    bash python -m venv venv source venv/bin/activate # Linux/Mac # 或 venv\Scripts\activate.bat (Windows) pip install pytest-rerunfailures

虚拟环境不仅能避免权限问题,还能实现项目级依赖隔离,简直是 Python 开发者的标配装备。


❌ 版本冲突?谁动了我的依赖?

有时候你会发现,明明装了最新版插件,却提示和当前 pytest 不兼容:

pytest-rerunfailures 12.3 requires pytest>=7.0, but you have pytest 6.2.5 which is incompatible.

这时候可以用 pip check 来诊断依赖冲突:

pip check

发现问题后,升级核心依赖即可:

pip install --upgrade pytest

当然,也可以降级插件来适配老框架,关键要看你的迁移节奏。

下面是常见问题速查表,建议收藏备用:

问题现象 可能原因 解决方案
找不到包 网络受限或源不可达 更换为可信镜像源
权限拒绝 全局路径无写权限 使用 --user 或虚拟环境
版本不兼容 插件与 pytest 主版本冲突 升级/降级对应包
命令未生效 插件未正确加载 运行 pytest --version 验证

顺便附上一个 Mermaid 流程图,帮你理清整个安装诊断逻辑:

graph TD
    A[开始安装 pytest-rerunfailures] --> B{是否在虚拟环境中?}
    B -->|否| C[创建虚拟环境]
    B -->|是| D[执行 pip install]
    D --> E{安装成功?}
    E -->|否| F[检查网络与镜像源]
    F --> G[更换 pip 源后重试]
    G --> H{成功?}
    H -->|否| I[检查权限]
    I --> J[使用 --user 或 sudo]
    J --> K[再次尝试安装]
    K --> L{仍失败?}
    L -->|是| M[手动下载 wheel 包本地安装]
    L -->|否| N[验证安装]
    E -->|是| N
    N --> O[运行 pytest --version 确认插件存在]
    O --> P[安装完成]

这张图特别适合新人快速上手,也能作为 CI 脚本编写的参考蓝图。


命令行实战:灵活控制重试行为 ⚙️

一旦插件装好,就可以立刻开始用了。最直接的方式就是通过命令行参数,不需要改任何代码,非常适合调试阶段临时启用。

--reruns N :失败了最多再给你 N 次机会

这是最核心的参数。语法如下:

pytest --reruns 3

意思是:每个失败的测试用例最多重试 3 次。只要有一次通过,就算整体成功。

举个例子,写一个故意“抽风”的测试:

# test_flaky.py
import random
import pytest

def test_network_call():
    assert random.random() > 0.3  # 70% 成功率

运行:

pytest test_flaky.py --reruns 2 -v

可能看到这样的输出:

test_flaky.py::test_network_call FAILED [33%]
RETRY test_flaky.py::test_network_call
test_flaky.py::test_network_call FAILED [66%]
RETRY test_flaky.py::test_network_call
test_flaky.py::test_network_call PASSED [100%]

虽然前两次挂了,但第三次活了下来,最终结果是 ✅ 通过!

注意: --reruns 0 表示关闭重试(这也是默认值),相当于显式禁用功能。


结合 -rF ,只看失败项摘要 👀

当你跑几百个测试时,满屏的日志会让你眼花缭乱。这时候可以用 -rF 参数只显示失败项:

pytest --reruns 2 -rF

输出可能是:

FAILED test_sample.py::test_something - AssertionError
Retrying test_sample.py::test_something (2 remaining retries)

清爽多了吧?这对快速定位问题非常有帮助。

再加上 --tb=short 缩减堆栈信息:

pytest --reruns 2 -rF --tb=short

形成一条高效的问题排查链路。


日志中识别重试轨迹 🕵️‍♂️

理解重试过程中的状态流转对调试至关重要。Pytest 会在控制台明确标注每次动作:

test_retry.py::test_timeout_request FAILED
================================== RETRYING ==================================
test_retry.py::test_timeout_request RETRY
test_retry.py::test_timeout_request PASSED

每次重试都会标记为 RETRY ,最终汇总报告也会保留历史记录。

如果你想看得更细,可以开启详细模式:

pytest --reruns 3 --verbose --durations=5

其中 --durations=5 会列出耗时最长的 5 个用例,有助于识别哪些重试拖慢了整体执行时间。

常用参数组合一览:

参数组合 用途说明
--reruns 2 基础重试机制,适用于大多数场景
--reruns 2 --reruns-delay 1 添加延迟间隔,模拟真实恢复窗口
--reruns 2 -rF --tb=line 快速查看失败重试轨迹
--reruns 0 显式关闭重试,用于对比基准性能

你可以把这些打包成脚本,方便重复使用:

#!/bin/bash
# run_with_retry.sh
echo "Starting flaky test suite with retry=3..."

pytest \
  --reruns 3 \
  --reruns-delay 2 \
  -rF \
  --tb=short \
  --verbose \
  tests/flaky/

这个脚本可以直接集成到 Jenkins 或 GitHub Actions 中,实现标准化执行策略。


配置文件驱动:持久化设置才是王道 📁

命令行虽然方便,但不适合长期维护。每次都要敲一堆参数太累了,而且容易遗漏。

更好的方式是使用配置文件来定义默认行为。

在 pytest.ini 中设定全局策略

在项目根目录创建 pytest.ini :

[tool:pytest]
addopts = --reruns 3 --reruns-delay 1

保存之后,以后只要运行 pytest ,就会自动带上这些选项,再也不用手动输入啦~

除了 pytest.ini ,你也可以用 pyproject.toml :

[tool.pytest.ini_options]
addopts = "--reruns 3 --reruns-delay 1"

或者 setup.cfg ,形式略有不同但效果一样。

小贴士: addopts 是 Pytest 提供的机制,用来追加默认命令行参数。作用范围是全局,适用于所有测试。


只对特定标记的用例重试:精准打击 🎯

并不是所有测试都应该重试。单元测试要是失败了,大概率是真有问题,重试也没用。

所以我们希望只对那些“易波动”的用例启用重试机制。

怎么做?答案是: 标记(marker)+ --reruns-only

先给容易受外部影响的测试打个标签:

import pytest

@pytest.mark.flaky
def test_external_api():
    import requests
    resp = requests.get("https://api.example.com/health", timeout=2)
    assert resp.status_code == 200

def test_local_logic():
    assert 1 + 1 == 2  # 不会被重试

然后在配置文件中加上:

[tool:pytest]
addopts = --reruns 3 --reruns-only

这样一来,只有打了 @pytest.mark.flaky 的用例才会被重试,其他的即使失败也不会重跑。

这招非常高明——既提升了稳定性,又避免了对确定性失败的无效挣扎,还能减少总执行时间。


多环境差异化配置:开发 vs 测试 vs CI 🔄

不同的环境对稳定性的容忍度不一样:

环境 重试次数 是否启用 说明
开发环境 0 否 快速反馈,便于调试
测试环境 2 是 容忍一定波动
CI/CD 环境 3 是 最大限度保证稳定性

为了实现这种差异,我们可以采用多配置文件策略:

project/
├── pytest.ini               # 默认配置(开发)
├── pytest.test.ini          # 测试环境
└── pytest.ci.ini            # CI 环境

例如 pytest.ci.ini 内容:

[tool:pytest]
addopts = --reruns 3 --reruns-delay 2 --reruns-only -v

然后在 CI 脚本中动态选择:

if [ "$ENV" == "ci" ]; then
  config_file="pytest.ci.ini"
else
  config_file="pytest.ini"
fi

pytest -c $config_file

这种方式实现了配置解耦,灵活性大大增强。


实战演练:模拟网络波动下的重试表现 🌐

理论讲再多不如动手试试。下面我们来构建一个贴近真实的测试案例。

编写一个“抽风”的 API 客户端

# tests/test_api_client.py
import time
import random
import pytest

class APIClient:
    def get_status(self):
        if random.random() < 0.3:
            raise ConnectionError("Network timeout")
        return {"status": "OK"}

client = APIClient()

@pytest.mark.flaky
def test_api_health_check():
    start = time.time()
    result = client.get_status()
    duration = time.time() - start
    print(f"\n⏱️ API call took {duration:.2f}s")
    assert result["status"] == "OK"

这个测试有 30% 的概率抛出连接超时异常,模拟弱网环境。

运行命令:

pytest tests/test_api_client.py --reruns 3 --reruns-delay 1 -s -v

可能会看到这样的输出:

test_api_client.py::test_api_health_check FAILED
⏱️ API call took 0.01s
================================== RETRYING ==================================
test_api_client.py::test_api_health_check RETRY
test_api_client.py::test_api_health_check PASSED
⏱️ API call took 0.00s

第一次失败 → 触发重试 → 第二次成功 → 终止流程 ✅

是不是感觉心里踏实多了?


状态机视角:重试到底经历了什么?

我们可以用 Mermaid 画出完整的状态流转图:

stateDiagram-v2
    [*] --> InitialRun
    InitialRun --> Failed : 失败
    Failed --> Retry1 : 触发重试(剩余3次)
    Retry1 --> Failed : 再次失败
    Failed --> Retry2 : 触发重试(剩余2次)
    Retry2 --> Success : 成功
    Success --> [*]
    Retry2 --> Failed
    Failed --> Retry3
    Retry3 --> Success : 最终成功
    Retry3 --> Failed : 全部重试耗尽
    Failed --> FinalFailure
    FinalFailure --> [*]

这张图清晰展示了从初始执行到最终结论的所有可能路径。无论中间经历多少波折,最终只有一个出口:要么通过,要么彻底失败。


报告输出结构分析:失败历史不该被抹去 🧾

即使测试最终通过了,我们也应该知道它是怎么“死里逃生”的。

安装 pytest-html 插件生成可视化报告:

pip install pytest-html
pytest --html=report.html --reruns 3

打开 report.html 你会发现:

  • 原始失败记录仍然保留;
  • 每次重试都有独立条目;
  • 最终状态为 PASSED ;
  • 控制台输出包含所有 print 日志。

这对于后期根因分析极其重要——即便最终通过,也能追溯哪几次尝试失败,辅助判断服务稳定性趋势。


深入原理:重试背后的调度机制揭秘 🔍

你以为 pytest-rerunfailures 只是简单地“重新运行”一次测试?Too young too simple.

实际上,它利用了 Pytest 强大的钩子系统,在测试生命周期中精确捕获状态变化,并通过两阶段执行策略实现智能重试。

两阶段执行:先跑一轮,再补漏

Pytest 默认是一次性遍历所有测试用例,无法中途插入新任务。因此, rerunfailures 采用了“两阶段执行”策略:

  1. 第一阶段 :完整执行所有测试;
  2. 第二阶段 :仅重跑那些失败且未达最大重试次数的用例。

流程如下:

graph TD
    A[开始测试会话] --> B{执行原始测试集}
    B --> C[记录所有失败用例 nodeid]
    C --> D{是否存在未达最大重试次数的失败?}
    D -- 是 --> E[构造新的执行计划]
    E --> F[仅包含需重试的用例]
    F --> G[再次调用 pytest.main()]
    G --> H[更新最终结果]
    D -- 否 --> I[结束会话, 输出报告]

关键在于,每次重试都是独立执行的,但结果会被合并到原始条目中,保持报告结构整洁。


重试间隔控制:固定延迟 or 指数退避?

频繁重试可能加剧系统负载。为此, --reruns-delay 支持设置固定延迟:

--reruns-delay=2  # 每次重试前等 2 秒

但对于更复杂的场景,建议使用指数退避(Exponential Backoff)策略:

import time
import random

def calculate_backoff_delay(attempt: int, base_delay: float = 1.0) -> float:
    exponential = base_delay * (2 ** (attempt - 1))  # 1s, 2s, 4s...
    jitter = random.uniform(0, 0.5)  # 防止雪崩
    return min(exponential + jitter, 30.0)  # 上限 30 秒

这种策略在面对瞬时故障时成功率更高,尤其适合调用第三方 API 的场景。


高级应用场景:真实世界的三大挑战 🧪

场景一:网络请求不稳定

微服务架构下,HTTP 请求失败太常见了。DNS 解析超时、TLS 握手失败、CDN 缓存未命中……随便一个都能让你的 CI 报红。

解决办法:结合标记机制精准重试。

@pytest.mark.flaky_network
def test_fetch_user_data():
    response = requests.get("https://api.example.com/users/123", timeout=5)
    assert response.status_code == 200

运行时只针对这类用例启用重试:

pytest --reruns 3 -m flaky_network

实测数据显示,在 10% 丢包率的网络条件下,原本失败率高达 68% 的测试集,经三次重试后通过率提升至 97.3%!


场景二:数据库连接中断

主从切换、连接池耗尽、容器重启……数据库瞬时不可达几乎是家常便饭。

解决方案是在每次重试前强制关闭旧连接:

from django.db import connection

@pytest.fixture(autouse=True)
def reset_db_connection():
    yield
    connection.close()  # 下次获取新连接

确保每次重试都在干净的连接上下文中执行,避免事务污染。


场景三:并发资源抢占

多个 CI job 同时写同一个临时文件?端口冲突?缓存 key 冲突?这些都是典型的竞争条件。

虽然理想方案是完全隔离资源,但在遗留系统中很难做到。此时适度重试是个不错的兜底手段:

pytest --reruns 3 --reruns-delay 0.5 -k test_write_config

配合短延迟,多数情况下可在竞争窗口过后顺利完成操作。

统计数据显示,适度重试可将通过率从 62% 提升至 96.8%,代价是平均耗时增加约 10 秒。


与主流插件协同工作:打造企业级测试体系 🏗️

和 pytest-xdist 并行执行共舞

并行测试能显著缩短执行时间,但和重试机制结合时要注意几点:

  • 重试由主进程统一调度;
  • 同一个用例可能在不同 worker 上执行;
  • 共享资源需做好隔离。

推荐配置:

[tool:pytest]
addopts = 
    -n auto
    --reruns 2
    --reruns-delay 1
    --dist=loadgroup

性能测试表明,在 500 个测试用例中(10% 为 flaky), xdist + reruns 组合在小幅增加时间的前提下,将成功率从 90.2% 提升至 98.9%,是目前最优解。


和 pytest-cov 覆盖率统计无缝集成

很多人担心重试会导致覆盖率数据失真。其实只要配置得当,完全没问题。

coverage.py 默认采用布尔并集合并策略,多次执行同一行代码仍只算一次覆盖。

建议措施:

  • 使用 COVERAGE_FILE 统一输出路径;
  • 清理旧 .coverage* 文件;
  • 禁止在 fixture 中启动额外 coverage 实例。

最终生成的 HTML 报告不会显示“执行 3 次”,只会标记为“已覆盖”,完全符合预期。


CI/CD 最佳实践:让流水线更健壮 🚀

多环境差异化策略

通过环境变量动态控制重试次数:

case $CI_ENV in
  "staging") RETRY_NUM=2 ;;
  "production") RETRY_NUM=3 ;;
  *) RETRY_NUM=0 ;;
esac

pytest --reruns $RETRY_NUM tests/

开发环境快速暴露问题,生产环境最大限度保障稳定性。


结合 Allure 报告可视化重试历史

Allure 天然支持重试展示,每个用例的多次执行痕迹都能清晰呈现:

pip install allure-pytest
pytest --reruns 2 --alluredir=./allure-results
allure serve ./allure-results

点击即可查看每次执行的堆栈、耗时与日志,极大提升调试效率。


自动归类偶发失败,推动持续优化

编写脚本分析 JUnit XML,找出长期依赖重试才能通过的“技术债”用例:

# analyze_flaky.py
import xml.etree.ElementTree as ET

tree = ET.parse('results.xml')
flaky_count = 0
for testcase in tree.findall('.//testcase'):
    if len(testcase.findall('failure')) > 0 and 'retry' in testcase.attrib.get('name', '').lower():
        print(f"Flaky test: {testcase.attrib['name']}")
        flaky_count += 1

print(f"Detected {flaky_count} flaky tests needing review.")

定期输出这类报告,促进团队主动修复不稳定测试。


总结:稳定不是靠运气,而是靠设计 🎯

pytest-rerunfailures 不是万能药,但它是一把利器。它不能修复代码缺陷,但能过滤掉干扰信号,让我们更专注于真正重要的问题。

它的价值不仅体现在“让 CI 变绿”,更在于:

  • 提升开发者信心,减少无效沟通;
  • 增强自动化测试的可信度;
  • 推动团队识别和治理不稳定测试;
  • 为企业级质量保障体系提供坚实基础。

当你把这套机制融入 CI/CD 流程后,你会发现:
构建失败不再令人焦虑,反而成为发现深层问题的起点。

而这,正是高质量交付的本质所在。✨

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介: pytest-rerunfailures 是一个增强 pytest 测试框架功能的实用插件,支持自动重跑失败的测试用例,有效应对CI环境中因网络波动、资源竞争等临时性问题导致的测试失败。通过命令行参数或配置文件可灵活设置重试次数和策略,提升测试稳定性与开发效率。本文介绍该插件的安装、配置及与其他常用插件(如pytest-xdist、pytest-cov)协同使用的最佳实践,帮助构建高效可靠的自动化测试体系。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐