在自动化测试中,测试失败往往并非由代码缺陷引起,而是由于环境不稳定、资源竞争或网络抖动等临时性问题。pytest-rerunfailures 插件提供了强大的失败重试机制,本教程将全面解析其使用方法和最佳实践。

一、为什么需要失败重试?

1.1 测试不稳定的常见原因

问题类型典型案例影响
环境因素服务启动延迟、资源不足30%的失败
网络问题API超时、连接中断25%的失败
并发竞争数据竞争、死锁20%的失败
测试依赖外部服务不可用15%的失败
随机故障偶然性错误10%的失败

1.2 重试机制的价值

成功
多次失败
第一次失败
重试机制
减少误报
发现真实缺陷

二、环境安装与配置

2.1 安装插件

pip install pytest-rerunfailures

2.2 基础配置方式

方式1:命令行参数

pytest --reruns 3      # 重试3次
pytest --reruns 5 --reruns-delay 2  # 重试5次,每次间隔2秒

方式2:配置文件 (pytest.ini)

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

方式3:标记特定测试

@pytest.mark.flaky(reruns=5, reruns_delay=1)
def test_unstable_api():
    ...

三、基础使用场景

3.1 全局重试所有失败测试

# 重试所有失败测试3次
pytest --reruns 3

# 重试5次,每次间隔2秒
pytest --reruns 5 --reruns-delay 2

3.2 特定测试重试

import pytest
import random

# 标记单个测试
@pytest.mark.flaky(reruns=3)
def test_flaky_login():
    assert random.random() > 0.3  # 70%失败率

# 标记整个测试类
@pytest.mark.flaky(reruns=5, reruns_delay=1)
class TestPayment:
    def test_credit_card(self):
        ...
    
    def test_paypal(self):
        ...

四、高级功能详解

4.1 条件重试

# conftest.py
def pytest_configure(config):
    # 注册自定义条件重试标记
    config.addinivalue_line(
        "markers",
        "retry_on(condition, reruns=3, delay=0.5): 条件重试标记"
    )

def pytest_runtest_setup(item):
    """动态应用重试策略"""
    for marker in item.iter_markers("retry_on"):
        condition = marker.kwargs["condition"]
        if condition == "network":
            item.add_marker(pytest.mark.flaky(
                reruns=marker.kwargs.get("reruns", 5),
                reruns_delay=marker.kwargs.get("delay", 1)
            ))
# test_api.py
@pytest.mark.retry_on(condition="network", reruns=3, delay=2)
def test_api_connection():
    """仅在网络错误时重试"""
    result = call_external_api()
    assert result.status_code == 200

4.2 重试后清理

def test_db_operation():
    try:
        # 测试执行
        db.insert(test_data)
        assert db.query() == expected
    finally:
        # 重试前清理
        db.delete(test_data.id)

4.3 重试间隔策略

import math

def exponential_backoff(attempt):
    """指数退避策略"""
    return 0.5 * math.pow(2, attempt)

@pytest.fixture(autouse=True)
def rerun_with_backoff(request):
    """应用指数退避重试"""
    if request.config.getoption("--reruns"):
        delay = exponential_backoff(request.session.testsfailed)
        request.config.option.reruns_delay = delay

五、企业级最佳实践

5.1 与 Allure 报告集成

# conftest.py
def pytest_runtest_logreport(report):
    """在报告中记录重试信息"""
    if hasattr(report, "wasxfail"):
        return
    
    if report.outcome == "failed" and hasattr(report, "rerun"):
        allure.dynamic.description(
            f"失败重试记录: {report.rerun}/{report.max_rerun}次重试"
        )
        allure.attach(
            f"重试间隔: {report.rerun_delay}秒",
            "重试策略",
            allure.attachment_type.TEXT
        )

5.2 CI/CD 集成配置

# .gitlab-ci.yml
stages:
  - test

test:
  stage: test
  script:
    - pip install pytest pytest-rerunfailures
    # 开发环境重试3次,生产环境仅重试1次
    - |
      if [ "$CI_ENVIRONMENT" = "dev" ]; then
        RERUNS=3
      else
        RERUNS=1
      fi
      pytest --reruns=$RERUNS --reruns-delay=1

5.3 重试分析报告

# conftest.py
RERUN_STATS = {}

def pytest_runtest_logstart(nodeid, location):
    """追踪重试情况"""
    RERUN_STATS[nodeid] = RERUN_STATS.get(nodeid, 0)

def pytest_runtest_logreport(report):
    """记录重试数据"""
    if report.outcome == "rerun":
        RERUN_STATS[report.nodeid] += 1

def pytest_terminal_summary(terminalreporter):
    """生成重试报告"""
    if not RERUN_STATS:
        return
    
    terminalreporter.write_sep("=", "重试统计报告")
    for test, count in sorted(RERUN_STATS.items(), key=lambda x: x[1], reverse=True):
        if count > 0:
            terminalreporter.write_line(f"- {test}: {count}次重试")

六、实战案例分析

6.1 电商支付测试

import pytest
import time

@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_payment_gateway():
    """支付网关重试测试"""
    # 模拟支付过程
    transaction_id = start_payment()
    
    # 网关处理可能需要时间
    time.sleep(1)
    
    # 验证支付状态
    status = check_payment_status(transaction_id)
    assert status == "COMPLETED"

def start_payment():
    """模拟支付启动(50%成功率)"""
    import random
    if random.random() > 0.5:
        return "txn_123456"
    raise ConnectionError("支付网关连接失败")

def check_payment_status(txn_id):
    """模拟支付状态检查"""
    return "COMPLETED"

七、注意事项与陷阱

7.1 不适合重试的场景

场景类型原因替代方案
确定性失败总是失败修复测试/缺陷
数据污染重试加剧问题更严格的清理
性能测试改变执行时间独立环境
顺序依赖改变执行顺序使用pytest-ordering

7.2 避免过度使用

  • 生产环境:禁用或最小化重试(最多1-2次)

  • 关键测试:记录完整重试日志

  • 重试上限:根据测试重要性设置不同阈值:

    # conftest.py
    def pytest_collection_modifyitems(items):
        for item in items:
            if "critical" in item.keywords:
                item.add_marker(pytest.mark.flaky(reruns=5))
            elif "high_priority" in item.keywords:
                item.add_marker(pytest.mark.flaky(reruns=3))
            else:
                item.add_marker(pytest.mark.flaky(reruns=1))
    

八、性能优化策略

8.1 并行执行优化

# 使用xdist并行执行同时支持重试
pytest -n 4 --reruns 2

8.2 智能重试策略

# conftest.py
def should_retry(excinfo):
    """智能重试判断"""
    exception_type = excinfo.type
    
    # 网络错误重试
    if exception_type in [ConnectionError, TimeoutError]:
        return True
    
    # 特定状态码重试
    if hasattr(excinfo.value, "status_code"):
        if excinfo.value.status_code in [502, 503, 504]:
            return True
    
    return False

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """自定义重试决策"""
    outcome = yield
    report = outcome.get_result()
    
    if report.failed and should_retry(call.excinfo):
        # 设置重试标记
        setattr(report, "should_retry", True)

九、完整工作流示例

9.1 项目结构

ecommerce-tests/
├── conftest.py              # 重试配置
├── pytest.ini               # 基础配置
├── tests/
│   ├── test_payment.py      # 支付测试
│   └── test_inventory.py    # 库存测试
└── run_tests.sh             # 执行脚本

9.2 pytest.ini 配置

[pytest]
addopts = 
    -v
    --reruns 2
    --reruns-delay 1
    --only-rerun AssertionError 
    --only-rerun ".*Connection.*"
    
markers =
    critical: 关键路径测试
    unstable: 不稳定测试(额外重试)

9.3 执行与报告

# 运行测试
pytest

# 输出示例
========================= test session starts =========================
platform linux -- Python 3.8.10, pytest-7.0.1, pluggy-1.0.0
plugins: rerunfailures-10.2
collected 23 items

test_payment.py::test_credit_card RERUN                    [ 10%]
test_payment.py::test_credit_card PASSED                    [ 20%]
test_inventory.py::test_stock_update FAILED (首次失败)        [ 30%]
test_inventory.py::test_stock_update RERUN                  [ 40%]
test_inventory.py::test_stock_update PASSED                 [ 50%]

================= 21 passed, 2 rerun in 15.32 seconds =================

十、扩展功能:重试分析

10.1 重试趋势监控

# 记录重试历史到CSV
import csv
import time

def record_retry_history(stats):
    with open("retry_history.csv", "a", newline="") as f:
        writer = csv.writer(f)
        for test, count in stats.items():
            writer.writerow([test, count, time.strftime("%Y-%m-%d")])

10.2 Jenkins集成

pipeline {
    agent any
    stages {
        stage('Test') {
            steps {
                script {
                    // 开发分支允许更多重试
                    if (env.BRANCH_NAME == 'develop') {
                        RERUNS = 5
                    } else {
                        RERUNS = 2
                    }
                    sh "pytest --reruns=${RERUNS} --reruns-delay=1"
                }
            }
            post {
                always {
                    // 生成重试报告
                    sh "python generate_retry_report.py"
                    archiveArtifacts 'retry_report.html'
                }
            }
        }
    }
}

总结

使用建议

  1. 分层重试策略:根据测试重要性配置不同重试次数
  2. 智能过滤:仅重试临时性问题,而非真实缺陷
  3. 适当间隔:根据系统恢复时间设置重试间隔
  4. 完整记录:在报告中记录所有重试行为
  5. 性能监控:追踪重试对整体执行时间的影响

pytest-rerunfailures 通过提供灵活的失败重试机制,可以显著提高测试套件的稳定性和可靠性,减少因环境问题导致的误报,同时帮助识别真正需要修复的问题。

Logo

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

更多推荐