【pytest】如何使用 pytest-rerunfailures 外掛並自定義重跑操作

adogs發表於2024-07-29

【pytest】如何使用 pytest-rerunfailures 外掛並自定義重跑操作

作用介紹

pytest-rerunfailures 外掛可以使測試用例在執行測試失敗的時候重新執行,並且會執行測試用例相對應的 setup 和 teardown 方法。通常有測試用例失敗重跑需求的時候就可以使用這個外掛。

但是 rerunfailures 外掛無法控制用例在失敗重跑時進行一些額外操作,這對於有一些自定義需求的業務場景來說很不方便,因此本文主要來介紹如何配合 rerunfailures 外掛來進行一些自定義操作。

外掛下載:

pip install pytest-rerunfailures

使用方法

  • 命令列引數方法
  • 裝飾器註解方法

命令列引數方法

pytest --reruns 3

只需要在 pytest 的命令列後加上--reruns num即可,num 的值代表測試失敗時重跑的次數

裝飾器註解方法

@pytest.mark.flaky(reruns=5)
def test_complete_inspect_control():
    assert 1 == True

在測試用例上新增@pytest.mark.flaky(reruns=num)裝飾器,num 的值代表測試失敗時重跑的次數

自定義重跑配置

rerunfailures 裝飾器在測試失敗的時候會固定執行用例的 setup、call、teardown 方法,其中 call 方法代表用例本身的執行函式。

因此我們想要自定義用例重跑時的一些操作,可以從兩個方面入手。

分別是:

  • setup、teardown 方法
  • rerunfailures 原始碼

setup、teardown 方法

由於 setup 和 teardown 方法在每一個測試用例執行時都會執行一遍,而如果我們的自定義操作只針對於失敗重跑的用例,則需要在 setup 和 teardown 方法里加一些判斷條件。

我的方法是在測試用例的 class 物件中初始化一個 bool 變數,用來標誌當前的測試用例是否需要進行重跑時的自定義操作。即:

class TestCase:
    restart_app = False

這裡將 bool 變數命名為 restart_app,因為我的自定義操作是在失敗時重啟 app。

將這個變數初始化為 False,表示預設情況下當前執行的測試用例是不需要執行自定義操作的。

定義好了變數後,就該編寫 setup 函式里的自定義操作的邏輯了。setup 具體邏輯如下:

class TestCaseBase:

    restart_app = False

    def setup_method(self):
        if  self.restart_app:
            # 這裡想要自定義的操作
            self.restart_app = Flase # 失敗重跑時執行完自定義操作後需要將標誌復原

接下來還需要控制這個 restar_app 變數在什麼時候變為 True。

pytest 框架有一個鉤子函式,名為pytest_runtest_makereport。在這個函式里,每一個執行完畢的測試用例,不論是否成功,都可以在這個函式里獲取他的測試結果,因此可以使用這個鉤子函式來控制標誌的值。

# conftest.py
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    result = yield
    rep = result.get_result()

    # 獲取setup、call、teardown方法的執行失敗的結果
    if rep.when in ("call", "setup", "teardown") and rep.failed:
        # 從報告中獲取失敗的詳細資訊
        failure_reason = rep.longrepr.reprcrash.message
        # 獲取失敗的測試用例名稱
        test_name = item.name
        # 列印或記錄失敗資訊
        logger.error(f"Test '{test_name}' failed with reason: {failure_reason}")
        if item.cls is not None:  # 確保測試用例屬於某個類
            item.cls.restart_app = True # 設定重啟標誌為True

到這一步就大功告成了,每一次測試用例執行失敗的時候,就會在pytest_runtest_makereport鉤子函式中捕獲到相應的資訊,並將該用例的父類的標誌物件設定為 True。接下來 rerunfailures 外掛就會進行重新執行該用例的 setup、call、teardown 函式。執行 setup 函式時,判斷到 restart_app 的值為 True,就會執行我們設定的自定義操作,執行完畢後將 restart_app 的值恢復為預設值。如此便實現了配合 rerunfailures 外掛執行自定義重跑操作。

修改 rerunfailures 原始碼

在一些需要高度自定義的需求下,單單隻配置 setup、teardown 方法可能不夠用,因此我們可以修改 rerunfailures 的原始碼來實現我們想要的效果。

首先我們要找到 rerunfailures 的原始碼檔案。在 python 虛擬環境中的路徑為:

.venv/Lib/site-packages/pytest_rerunfailures.py

如果使用的是 python 的全域性環境,只需要在 python 的安裝資料夾下的 lib 庫裡尋找即可。

開啟原始檔,找到pytest_runtest_protocol函式,這個函式也是 pytest 框架的鉤子函式,不過我們沒有必要自己使用這個鉤子函式實現一些重跑邏輯,直接在現有的輪子上做增量即可。

這個函式的原始碼如下:

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

整段程式碼看起來很長,但是我們只需要重點關注兩個地方:runtestprotocolfor report in reports

runtestprotocol函式是控制整個測試用例執行 setup、call、teardown 方法的。
for report in reports則是針對三個方法來分別識別其執行結果的,我們的一些自定義邏輯基本就寫在這個迴圈下面。

通常來說,如果我們需要在測試用例執行前(setup、call、teardwon)執行一些操作的話,則可以在runtestprotocol函式前執行,如:

# do something special
reports = runtestprotocol(item, nextitem=nextitem, log=False)

如果需要在測試用例執行結束後,獲取執行結果並進行操作的話,則需要在for report in reports迴圈下進行操作,如:

for report in reports:  # 3 reports: setup, call, teardown
    if report.outcome = "failed" :
        # do something special
        break
    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

這裡只是加了一個無論設定了多少次重跑次數,只要執行失敗(setup、call、teardown)都重新執行的邏輯。

通常大部分的需求會有一些更多的、額外的一些邏輯,需要大家自行處理。

相關文章