如何使用 skip 和 xfail 處理無法成功的測試

您可以標記在特定平台上無法運行的測試函數,或您預期會失敗的測試函數,以便 pytest 可以相應地處理它們並呈現測試會話的摘要,同時保持測試套件綠色

Skip 表示您預期您的測試只有在滿足某些條件時才會通過,否則 pytest 應完全跳過運行測試。常見的例子是在非 Windows 平台上跳過僅限 Windows 的測試,或跳過依賴於目前不可用的外部資源(例如資料庫)的測試。

Xfail 表示您預期測試會因某些原因而失敗。常見的例子是針對尚未實作的功能或尚未修復的錯誤進行測試。當預期會失敗(標記為 pytest.mark.xfail)的測試通過時,它會被視為 xpass,並將在測試摘要中報告。

pytest 會分別計數和列出 skipxfail 測試。預設情況下,不會顯示有關跳過/xfail 測試的詳細資訊,以避免輸出混亂。您可以使用 -r 選項來查看與測試進度中顯示的「簡短」字母相對應的詳細資訊

pytest -rxXs  # show extra info on xfailed, xpassed, and skipped tests

有關 -r 選項的更多詳細資訊,可以透過運行 pytest -h 找到。

(請參閱 內建組態檔案選項

跳過測試函數

跳過測試函數的最簡單方法是使用 skip 裝飾器標記它,該裝飾器可以傳遞一個可選的 reason

@pytest.mark.skip(reason="no way of currently testing this")
def test_the_unknown(): ...

或者,也可以透過調用 pytest.skip(reason) 函數在測試執行或設定期間命令式地跳過

def test_function():
    if not valid_config():
        pytest.skip("unsupported configuration")

當在導入時無法評估跳過條件時,命令式方法很有用。

也可以使用模組級別的 pytest.skip(reason, allow_module_level=True) 跳過整個模組

import sys

import pytest

if not sys.platform.startswith("win"):
    pytest.skip("skipping windows-only tests", allow_module_level=True)

參考pytest.mark.skip

skipif

如果您希望有條件地跳過某些內容,那麼您可以使用 skipif 來代替。以下是一個範例,說明如何在 Python 3.10 之前的直譯器上運行時跳過測試函數

import sys


@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher")
def test_function(): ...

如果條件在收集期間評估為 True,則將跳過測試函數,並且在使用 -rs 時,指定的理由將出現在摘要中。

您可以在模組之間共享 skipif 標記。考慮這個測試模組

# content of test_mymodule.py
import mymodule

minversion = pytest.mark.skipif(
    mymodule.__versioninfo__ < (1, 1), reason="at least mymodule-1.1 required"
)


@minversion
def test_function(): ...

您可以導入標記並在另一個測試模組中重複使用它

# test_myothermodule.py
from test_mymodule import minversion


@minversion
def test_anotherfunction(): ...

對於較大的測試套件,通常最好有一個檔案,您可以在其中定義標記,然後在整個測試套件中一致地應用這些標記。

或者,您可以使用 條件字串 而不是布林值,但它們不容易在模組之間共享,因此主要為了向後相容性而支援它們。

參考pytest.mark.skipif

跳過類別或模組的所有測試函數

您可以在類別上使用 skipif 標記(與任何其他標記一樣)

@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
class TestPosixCalls:
    def test_function(self):
        "will not be setup or run under 'win32' platform"

如果條件為 True,則此標記將為該類別的每個測試方法產生跳過結果。

如果您想跳過模組的所有測試函數,您可以使用 pytestmark 全域變數

# test_module.py
pytestmark = pytest.mark.skipif(...)

如果將多個 skipif 裝飾器應用於測試函數,則只要任何一個跳過條件為真,就會跳過它。

跳過檔案或目錄

有時您可能需要跳過整個檔案或目錄,例如,如果測試依賴於特定於 Python 版本的特性,或包含您不希望 pytest 運行的程式碼。在這種情況下,您必須從收集範圍中排除檔案和目錄。有關更多資訊,請參閱 自訂測試收集

因缺少導入依賴項而跳過

您可以透過在模組級別、測試內部或測試設定函數中使用 pytest.importorskip 來跳過因缺少導入而導致的測試。

docutils = pytest.importorskip("docutils")

如果在此處無法導入 docutils,這將導致測試結果為跳過。您也可以根據程式庫的版本號跳過

docutils = pytest.importorskip("docutils", minversion="0.3")

版本將從指定模組的 __version__ 屬性中讀取。

摘要

以下是在不同情況下如何在模組中跳過測試的快速指南

  1. 無條件跳過模組中的所有測試

pytestmark = pytest.mark.skip("all tests still WIP")
  1. 根據某些條件跳過模組中的所有測試

pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="tests for linux only")
  1. 如果缺少某些導入,則跳過模組中的所有測試

pexpect = pytest.importorskip("pexpect")

XFail:將測試函數標記為預期會失敗

您可以使用 xfail 標記來指示您預期測試會失敗

@pytest.mark.xfail
def test_function(): ...

此測試將運行,但失敗時不會報告回溯。相反,終端報告會將其列在「預期失敗」(XFAIL) 或「意外通過」(XPASS) 區段中。

或者,您也可以從測試內部或其設定函數中命令式地將測試標記為 XFAIL

def test_function():
    if not valid_config():
        pytest.xfail("failing configuration (but should work)")
def test_function2():
    import slow_module

    if slow_module.slow_function():
        pytest.xfail("slow_module taking too long")

這兩個範例說明了您不想在模組級別檢查條件的情況,在這種情況下,條件將針對標記進行評估。

這將使 test_function XFAIL。請注意,在 pytest.xfail() 調用之後,不會執行其他程式碼,這與標記不同。這是因為它在內部透過引發已知的例外情況來實作。

參考pytest.mark.xfail

condition 參數

如果測試僅在特定條件下預期會失敗,您可以將該條件作為第一個參數傳遞

@pytest.mark.xfail(sys.platform == "win32", reason="bug in a 3rd party library")
def test_function(): ...

請注意,您也必須傳遞一個理由(請參閱 pytest.mark.xfail 上的參數描述)。

reason 參數

您可以使用 reason 參數指定預期失敗的動機

@pytest.mark.xfail(reason="known parser issue")
def test_function(): ...

raises 參數

如果您想更具體地說明測試失敗的原因,您可以在 raises 參數中指定單個例外或例外元組。

@pytest.mark.xfail(raises=RuntimeError)
def test_function(): ...

然後,如果測試因 raises 中未提及的例外而失敗,則會將其報告為常規失敗。

run 參數

如果測試應標記為 xfail 並如此報告,但不應執行,請將 run 參數設定為 False

@pytest.mark.xfail(run=False)
def test_function(): ...

這對於 xfail 那些會使直譯器崩潰且應稍後調查的測試特別有用。

strict 參數

預設情況下,XFAILXPASS 都不会使測試套件失敗。您可以透過將 strict 僅限關鍵字參數設定為 True 來變更此行為

@pytest.mark.xfail(strict=True)
def test_function(): ...

這將使此測試的 XPASS(「意外通過」)結果導致測試套件失敗。

您可以使用 xfail_strict ini 選項變更 strict 參數的預設值

[pytest]
xfail_strict=true

忽略 xfail

透過在命令列中指定

pytest --runxfail

您可以強制運行和報告標記為 xfail 的測試,就像它根本沒有被標記一樣。這也會導致 pytest.xfail() 不產生任何效果。

範例

這是一個簡單的測試檔案,其中包含多種用法

from __future__ import annotations

import pytest


xfail = pytest.mark.xfail


@xfail
def test_hello():
    assert 0


@xfail(run=False)
def test_hello2():
    assert 0


@xfail("hasattr(os, 'sep')")
def test_hello3():
    assert 0


@xfail(reason="bug 110")
def test_hello4():
    assert 0


@xfail('pytest.__version__[0] != "17"')
def test_hello5():
    assert 0


def test_hello6():
    pytest.xfail("reason")


@xfail(raises=IndexError)
def test_hello7():
    x = []
    x[1] = 1

使用 report-on-xfail 選項運行它會產生以下輸出

! pytest -rx xfail_demo.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-1.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR/example
collected 7 items

xfail_demo.py xxxxxxx                                                [100%]

========================= short test summary info ==========================
XFAIL xfail_demo.py::test_hello
XFAIL xfail_demo.py::test_hello2
  reason: [NOTRUN]
XFAIL xfail_demo.py::test_hello3
  condition: hasattr(os, 'sep')
XFAIL xfail_demo.py::test_hello4
  bug 110
XFAIL xfail_demo.py::test_hello5
  condition: pytest.__version__[0] != "17"
XFAIL xfail_demo.py::test_hello6
  reason: reason
XFAIL xfail_demo.py::test_hello7
============================ 7 xfailed in 0.12s ============================

使用 parametrize 的 Skip/xfail

在使用 parametrize 時,可以將 skip 和 xfail 等標記應用於個別測試實例

import sys

import pytest


@pytest.mark.parametrize(
    ("n", "expected"),
    [
        (1, 2),
        pytest.param(1, 0, marks=pytest.mark.xfail),
        pytest.param(1, 3, marks=pytest.mark.xfail(reason="some bug")),
        (2, 3),
        (3, 4),
        (4, 5),
        pytest.param(
            10, 11, marks=pytest.mark.skipif(sys.version_info >= (3, 0), reason="py2k")
        ),
    ],
)
def test_increment(n, expected):
    assert n + 1 == expected