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

你可以標記無法在特定平台上執行或你預期會失敗的測試函式,讓 pytest 能夠適當地處理它們,並提供測試階段的摘要,同時保持測試套件綠色

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

xfail 表示你預期測試會因為某些原因失敗。常見範例是針對尚未實作的功能或尚未修正的錯誤進行測試。當測試通過,儘管預期會失敗(標記為 pytest.mark.xfail),它就是xpass,並會在測試摘要中報告。

pytest 會分別計算並列出skipxfail 測試。預設情況下,不會顯示跳過/失敗測試的詳細資訊,以避免輸出雜亂。你可以使用 -r 選項查看測試進度中顯示的「簡短」字母對應的詳細資訊

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

可以在執行 pytest -h 時找到 -r 選項的更多詳細資訊。

(請參閱 內建組態檔選項

跳過測試函式

跳過測試函式最簡單的方法是用 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 代替。以下是一個範例,說明當在早於 Python3.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 並報告為 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() 沒有產生任何效果。

範例

以下是包含多種用法的簡單測試檔案

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 跳過/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