lsekfe 发表于 2023-12-18 10:29:46

pytest-rerunfailures:优化测试稳定性的失败重试工具

1、前言
笔者在执行自动化测试用例时,会发现有时候用例失败并非代码问题,而是由于服务正在发版,导致请求失败,从而降低了自动化用例的稳定性,最后还要花时间定位到底是自身case的原因还是业务逻辑问题,还是其他原因,增加了定位成本。增加容错机制,失败重试,会解决大部分由于网络原因、服务重启等原因造成的case失败问题。那该如何增加失败重试机制呢?带着问题我们一起探索。
2、pytest-rerunfailures 插件先给出答案,我们将使用 pytest-rerunfailures 插件来实现失败重试功能。什么是 pytest-rerunfailures ?pytest-rerunfailures 是一个基于 pytest 框架的插件,它允许我们对测试用例进行失败重试。当一个测试用例失败时,插件会自动重新运行失败的测试用例,直到达到预定的重试次数或测试用例通过为止。这样可以增加用例的稳定性,并减少因为偶发性问题导致的测试失败。如何使用 pytest-rerunfailures ?
[*]方式一

首先,确保已经安装了 pytest-rerunfailures 插件。可以使用以下命令进行安装:<font size="3">pip install pytest-rerunfailures</font>安装完成后,在项目中使用 pytest 运行测试用例时,pytest-rerunfailures 插件会自动生效。
接下来,在编写测试用例时,可以通过添加 @pytest.mark.flaky 装饰器将需要重试的测试用例标记起来。例如:
test_demo.py<font size="3">
import pytest


@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_case():
    assert 1 == 2</font>在上述示例中,我们使用了 @pytest.mark.flaky 装饰器来标记测试用例 test_case 为可重试的。参数 reruns 指定了重试次数,而 reruns_delay 则指定了每次重试之间的延迟时间(以秒为单位)。
我们来运行case,看一下执行结果:
执行命令:pytest -s -v test_demo.py::test_case,会看到如下结果:<font size="3">RERUN
test_dir/test_demo.py::test_case RERUN
test_dir/test_demo.py::test_case RERUN
test_dir/test_demo.py::test_case FAILED</font>可以看到,重试了3次,最终结果为失败。
注意:如果你是在pycharm中点击绿色三角形直接运行是不生效的
总结一下:
当运行测试时,如果测试用例失败,pytest-rerunfailures 插件会根据我们配置的重试次数和延迟时间自动重新运行该测试用例,直到达到最大重试次数或测试通过为止。

[*]方式二
除了使用装饰器来标记测试用例外,pytest-rerunfailures 还支持使用命令行选项和配置文件的方式进行配置。命令行执行的话,可以这样写:pytest -s -v --reruns 3 --reruns-delay 2 test_demo.py::test_case或者代码运行的话,可以这样写:pytest.main(["-s", "-v", "--reruns", "3", "--reruns-delay", "2", "test_demo.py::test_case"])3、运行机制到这里,应该会使用了。我们简单概括一下它的运行机制:
1、pytest 通过插件系统加载 pytest-rerunfailures 插件,并启用其功能。2、当 pytest 运行测试时,对每个测试用例的执行进行监控。3、如果一个测试用例执行失败,pytest-rerunfailures 插件会捕获该失败,并判断是否需要进行重试。4、如果该测试用例被标记为可重试(使用了 @pytest.mark.flaky 装饰器),插件会根据配置的重试次数和延迟时间重新运行该测试用例。5、在每次重试之前,插件会根据设置的延迟时间暂停一段时间。6、如果测试用例在重试次数达到上限之前通过了,即成功执行,则插件会将该测试用例标记为通过。7、如果测试用例在达到最大重试次数后仍然失败,则插件会返回最后一次失败的结果作为最终的结果。
总结起来,pytest-rerunfailures 插件在测试执行失败时,根据配置的重试次数和延迟时间重新运行测试用例,并根据重试结果判断最终的测试结果。这样可以提高测试用例的稳定性,并减少偶发性问题导致的测试失败。
4、源码读解使用阶段,我们使用 mark 标记,那源码中应该添加了该标记。def pytest_configure(config):
    # add flaky marker
    config.addinivalue_line(
      "markers",
      "flaky(reruns=1, reruns_delay=0): mark test to re-run up "
      "to 'reruns' times. Add a delay of 'reruns_delay' seconds "
      "between re-runs.",
    )
    ......简单解释一下:

[*]pytest_configure(config) 是 pytest 的一个钩子函数,用于在 pytest 配置阶段对配置进行自定义操作。

[*]config.addinivalue_line() 是 pytest 的配置方法,用于向配置中添加新的配置项或配置信息。

[*]在这段代码中,通过 config.addinivalue_line() 方法,插件向 pytest 的配置中加入了一行字符串。

[*]这行字符串指定了标记名称为 "flaky",并使用参数 reruns 和 reruns_delay 来说明重试次数和延迟时间的默认值。

[*]标记的含义是将被标记的测试用例重新运行最多 "reruns" 次,每次重试之间间隔 "reruns_delay" 秒。

通过这个自定义的标记,就可以使用 @pytest.mark.flaky 装饰器来标记需要进行重试的测试用例,并且可以在装饰器中指定具体的重试次数和延迟时间。我们看看实现失败重试的源码,这才是重点。def pytest_runtest_protocol(item, nextitem):
    """
    Run the test protocol.

    Note: when teardown fails, two reports are generated for the case, one for
    the test case and the other for the teardown error.
    """
    reruns = get_reruns_count(item)
    if reruns is None:
      # global setting is not specified, and this test is not marked with
      # flaky
      return

    # while this doesn't need to be run with every item, it will fail on the
    # first item if necessary
    check_options(item.session.config)
    delay = get_reruns_delay(item)
    parallel = not is_master(item.config)
    db = item.session.config.failures_db
    item.execution_count = db.get_test_failures(item.nodeid)
    db.set_test_reruns(item.nodeid, reruns)

    if item.execution_count > reruns:
      return True

    need_to_run = True
    while need_to_run:
      item.execution_count += 1
      item.ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
      reports = runtestprotocol(item, nextitem=nextitem, log=False)

      for report in reports:# 3 reports: setup, call, teardown
            report.rerun = item.execution_count - 1
            if _should_not_rerun(item, report, reruns):
                # last run or no failure detected, log normally
                item.ihook.pytest_runtest_logreport(report=report)
            else:
                # failure detected and reruns not exhausted, since i < reruns
                report.outcome = "rerun"
                time.sleep(delay)

                if not parallel or works_with_current_xdist():
                  # will rerun test, log intermediate result
                  item.ihook.pytest_runtest_logreport(report=report)

                # cleanin item's cashed results from any level of setups
                _remove_cached_results_from_failed_fixtures(item)
                _remove_failed_setup_state_from_session(item)

                break# trigger rerun
      else:
            need_to_run = False

      item.ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location)

    return True简单解释一下:首先,通过函数 get_reruns_count(item) 获取当前测试用例需要重试的次数。如果没有设置重试次数,则直接返回。然后,检查配置选项并获取重试的延迟时间,并确定是否运行在并行模式下。还会获取失败记录数据库对象,并获取当前测试用例已经执行的次数。接下来,通过比较已执行次数和设定的重试次数,判断是否需要进行重试。如果已执行次数大于等于设定的重试次数,则不再进行重试,直接返回。如果需要重试,会进入一个循环,每次重试会增加已执行次数。在重试过程中,会调用 pytest_runtest_logstart 函数记录测试用例开始执行的日志。然后,通过调用 runtestprotocol 函数执行测试用例,并获取测试结果。在这里,生成的报告会被标记为执行次数减一,以便区分原始执行和重试执行的报告。接着,通过 _should_not_rerun 函数判断当前报告是否满足不需要重试的条件。如果满足,则继续执行后续操作。如果报告表明需要重试,并且重试次数未达到设定的次数,会将报告的结果设置为 "rerun",并根据设定的延迟时间暂停一段时间。然后,根据并行模式和当前使用的 xdist 版本,决定是否记录中间结果。同时,会清除缓存的结果和执行状态。之后,重试循环会继续,直到不满足重试条件为止。最后,会调用 pytest_runtest_logfinish 函数记录测试用例结束执行的日志。最后,函数返回 True,表示已经实现重试机制。总结起来,这段代码通过循环执行测试用例,并在满足重试条件时进行重试,直到满足退出条件为止。在重试过程中,会记录日志、生成报告,并根据设定的重试次数和延迟时间进行控制。
4、最后:失败重试功能并不是解决所有测试问题的法宝,它应该被视为一种提高测试稳定性的辅助手段。在使用 pytest-rerunfailures 进行失败重试时,我们应该仔细分析失败的原因,确保重试次数和延迟时间设置合理,并与团队成员共同讨论和决定是否需要重试测试用例。总结起来,pytest-rerunfailures 是一个非常有用的工具,可以提高测试用例的稳定性。通过使用它,我们可以轻松地实现失败重试功能,并减少由于偶发性问题导致的测试失败。另外源码中,看到了 pytest_runtest_logstart 等,可能有些同学不明白这是干嘛用的,之后我们专门写一篇文章来介绍它的作用。

页: [1]
查看完整版本: pytest-rerunfailures:优化测试稳定性的失败重试工具