如何捕獲警告

從版本 3.1 開始,pytest 現在會在測試執行期間自動捕獲警告,並在會話結束時顯示它們

# content of test_show_warnings.py
import warnings


def api_v1():
    warnings.warn(UserWarning("api v1, should use functions from v2"))
    return 1


def test_one():
    assert api_v1() == 1

現在執行 pytest 會產生此輸出

$ pytest test_show_warnings.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item

test_show_warnings.py .                                              [100%]

============================= warnings summary =============================
test_show_warnings.py::test_one
  /home/sweet/project/test_show_warnings.py:5: UserWarning: api v1, should use functions from v2
    warnings.warn(UserWarning("api v1, should use functions from v2"))

-- Docs: https://pytest.dev.org.tw/en/stable/how-to/capture-warnings.html
======================= 1 passed, 1 warning in 0.12s =======================

控制警告

類似於 Python 的 警告過濾器-W 選項 標誌,pytest 提供了自己的 -W 標誌來控制哪些警告被忽略、顯示或轉換為錯誤。 有關更進階的使用案例,請參閱警告過濾器文件。

此程式碼範例示範如何將任何 UserWarning 類別的警告視為錯誤

$ pytest -q test_show_warnings.py -W error::UserWarning
F                                                                    [100%]
================================= FAILURES =================================
_________________________________ test_one _________________________________

    def test_one():
>       assert api_v1() == 1

test_show_warnings.py:10:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

    def api_v1():
>       warnings.warn(UserWarning("api v1, should use functions from v2"))
E       UserWarning: api v1, should use functions from v2

test_show_warnings.py:5: UserWarning
========================= short test summary info ==========================
FAILED test_show_warnings.py::test_one - UserWarning: api v1, should use ...
1 failed in 0.12s

相同的選項可以在 pytest.inipyproject.toml 檔案中使用 filterwarnings ini 選項進行設定。 例如,以下配置將忽略所有使用者警告和符合正則表達式的特定棄用警告,但會將所有其他警告轉換為錯誤。

# pytest.ini
[pytest]
filterwarnings =
    error
    ignore::UserWarning
    ignore:function ham\(\) is deprecated:DeprecationWarning
# pyproject.toml
[tool.pytest.ini_options]
filterwarnings = [
    "error",
    "ignore::UserWarning",
    # note the use of single quote below to denote "raw" strings in TOML
    'ignore:function ham\(\) is deprecated:DeprecationWarning',
]

當警告符合列表中的多個選項時,將執行最後一個符合選項的操作。

注意

-W 標誌和 filterwarnings ini 選項使用結構相似的警告過濾器,但每個配置選項對其過濾器的解釋方式不同。 例如,filterwarnings 中的 *message* 是一個字串,其中包含警告訊息開頭必須符合的正則表達式(不區分大小寫),而 -W 中的 *message* 是一個文字字串,警告訊息的開頭必須包含該字串(不區分大小寫),並忽略訊息開頭或結尾的任何空格。 有關更多詳細資訊,請參閱警告過濾器文件。

@pytest.mark.filterwarnings

您可以使用 @pytest.mark.filterwarnings 標記將警告過濾器新增到特定的測試項目,讓您可以更精細地控制應在測試、類別甚至模組級別捕獲哪些警告

import warnings


def api_v1():
    warnings.warn(UserWarning("api v1, should use functions from v2"))
    return 1


@pytest.mark.filterwarnings("ignore:api v1")
def test_one():
    assert api_v1() == 1

您可以使用單獨的裝飾器指定多個過濾器

# Ignore "api v1" warnings, but fail on all other warnings
@pytest.mark.filterwarnings("ignore:api v1")
@pytest.mark.filterwarnings("error")
def test_one():
    assert api_v1() == 1

重要

關於裝飾器順序和過濾器優先順序:請務必記住,裝飾器的評估順序是相反的,因此您必須以與傳統 warnings.filterwarnings()-W 選項 用法相反的順序列出警告過濾器。 這實際上表示,來自較早的 @pytest.mark.filterwarnings 裝飾器的過濾器優先於來自較晚的裝飾器的過濾器,如上面的範例所示。

使用標記套用的過濾器優先於在命令列上傳遞或由 filterwarnings ini 選項配置的過濾器。

您可以透過使用 filterwarnings 標記作為類別裝飾器,將過濾器套用到類別的所有測試,或者透過設定 pytestmark 變數來套用到模組中的所有測試

# turns all warnings into errors for this module
pytestmark = pytest.mark.filterwarnings("error")

注意

如果您想要套用多個過濾器(透過將 filterwarnings 標記的列表指派給 pytestmark),則必須使用傳統的 warnings.filterwarnings() 排序方法(較晚的過濾器優先),這與上面提到的裝飾器方法相反。

感謝 Florian Schulze 在 pytest-warnings 插件中提供的參考實作。

停用警告摘要

雖然不建議,但您可以使用 --disable-warnings 命令列選項來完全從測試執行輸出中抑制警告摘要。

完全停用警告捕獲

此插件預設為啟用,但可以在您的 pytest.ini 檔案中使用以下方式完全停用

[pytest]
addopts = -p no:warnings

或在命令列中傳遞 -p no:warnings。 如果您的測試套件使用外部系統處理警告,這可能會很有用。

DeprecationWarning 和 PendingDeprecationWarning

預設情況下,pytest 將顯示來自使用者程式碼和第三方庫的 DeprecationWarningPendingDeprecationWarning 警告,正如 PEP 565 所建議的那樣。 這有助於使用者保持其程式碼的現代性,並避免在有效移除棄用警告時發生中斷。

但是,在使用者捕獲測試中任何類型的警告的特定情況下,無論是使用 pytest.warns()pytest.deprecated_call() 還是使用 recwarn fixture,都不會顯示任何警告。

有時,隱藏您無法控制的程式碼(例如第三方庫)中發生的某些特定棄用警告很有用,在這種情況下,您可以使用警告過濾器選項(ini 或標記)來忽略這些警告。

例如

[pytest]
filterwarnings =
    ignore:.*U.*mode is deprecated:DeprecationWarning

這將忽略所有類型為 DeprecationWarning 的警告,其中訊息的開頭符合正則表達式 ".*U.*mode is deprecated"

有關更多範例,請參閱 @pytest.mark.filterwarnings控制警告

注意

如果在直譯器層級配置了警告,使用 PYTHONWARNINGS 環境變數或 -W 命令列選項,pytest 預設不會配置任何過濾器。

此外,pytest 不遵循 PEP 506 關於重置所有警告過濾器的建議,因為這可能會破壞透過調用 warnings.simplefilter() 配置警告過濾器本身的測試套件(有關範例,請參閱#2430)。

確保程式碼觸發棄用警告

您也可以使用 pytest.deprecated_call() 來檢查特定函數調用是否觸發 DeprecationWarningPendingDeprecationWarning

import pytest


def test_myfunction_deprecated():
    with pytest.deprecated_call():
        myfunction(17)

如果使用 17 參數調用 myfunction 時未發出棄用警告,則此測試將失敗。

使用 warns 函數斷言警告

您可以使用 pytest.warns() 檢查程式碼是否引發特定警告,其工作方式與 raises 類似(除了 raises 不會捕獲所有異常,只會捕獲 expected_exception

import warnings

import pytest


def test_warning():
    with pytest.warns(UserWarning):
        warnings.warn("my warning", UserWarning)

如果未引發相關警告,則測試將失敗。 使用關鍵字引數 match 來斷言警告是否符合文字或正則表達式。 若要比對可能包含正則表達式元字元(如 (.)的文字字串,可以先使用 re.escape 逸出模式。

一些範例

>>> with warns(UserWarning, match="must be 0 or None"):
...     warnings.warn("value must be 0 or None", UserWarning)
...

>>> with warns(UserWarning, match=r"must be \d+$"):
...     warnings.warn("value must be 42", UserWarning)
...

>>> with warns(UserWarning, match=r"must be \d+$"):
...     warnings.warn("this is not here", UserWarning)
...
Traceback (most recent call last):
  ...
Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...

>>> with warns(UserWarning, match=re.escape("issue with foo() func")):
...     warnings.warn("issue with foo() func")
...

您也可以在函數或程式碼字串上調用 pytest.warns()

pytest.warns(expected_warning, func, *args, **kwargs)
pytest.warns(expected_warning, "func(*args, **kwargs)")

該函數還會傳回所有引發警告的列表(作為 warnings.WarningMessage 物件),您可以查詢這些物件以獲取更多資訊

with pytest.warns(RuntimeWarning) as record:
    warnings.warn("another warning", RuntimeWarning)

# check that only one warning was raised
assert len(record) == 1
# check that the message matches
assert record[0].message.args[0] == "another warning"

或者,您可以使用 recwarn fixture 詳細檢查引發的警告(請參閱下方)。

recwarn fixture 自動確保在測試結束時重置警告過濾器,因此不會洩漏全域狀態。

記錄警告

您可以使用 pytest.warns() 上下文管理器或 recwarn fixture 來記錄引發的警告。

若要使用 pytest.warns() 記錄警告而不斷言任何關於警告的內容,請傳遞不帶引數的預期警告類型,它將預設為通用 Warning

with pytest.warns() as record:
    warnings.warn("user", UserWarning)
    warnings.warn("runtime", RuntimeWarning)

assert len(record) == 2
assert str(record[0].message) == "user"
assert str(record[1].message) == "runtime"

recwarn fixture 將記錄整個函數的警告

import warnings


def test_hello(recwarn):
    warnings.warn("hello", UserWarning)
    assert len(recwarn) == 1
    w = recwarn.pop(UserWarning)
    assert issubclass(w.category, UserWarning)
    assert str(w.message) == "hello"
    assert w.filename
    assert w.lineno

recwarn fixture 和 pytest.warns() 上下文管理器都傳回相同的介面來記錄警告:WarningsRecorder 實例。 若要查看記錄的警告,您可以迭代此實例,調用 len 以取得記錄的警告數量,或索引到其中以取得特定的記錄警告。

測試中警告的其他使用案例

以下是一些涉及警告的使用案例,這些案例經常在測試中出現,以及關於如何處理它們的建議

  • 若要確保發出至少一個指示的警告,請使用

def test_warning():
    with pytest.warns((RuntimeWarning, UserWarning)):
        ...
  • 若要確保發出某些警告,請使用

def test_warning(recwarn):
    ...
    assert len(recwarn) == 1
    user_warning = recwarn.pop(UserWarning)
    assert issubclass(user_warning.category, UserWarning)
  • 若要確保發出任何警告,請使用

def test_warning():
    with warnings.catch_warnings():
        warnings.simplefilter("error")
        ...
  • 若要抑制警告,請使用

with warnings.catch_warnings():
    warnings.simplefilter("ignore")
    ...

自訂失敗訊息

記錄警告提供了在未發出警告或滿足其他條件時產生自訂測試失敗訊息的機會。

def test():
    with pytest.warns(Warning) as record:
        f()
        if not record:
            pytest.fail("Expected a warning!")

如果在調用 f 時未發出警告,則 not record 將評估為 True。 然後,您可以使用自訂錯誤訊息調用 pytest.fail()

內部 pytest 警告

pytest 在某些情況下可能會產生自己的警告,例如不當使用或已棄用的功能。

例如,如果 pytest 遇到一個符合 python_classes 的類別,但也定義了 __init__ 建構函數,pytest 將發出警告,因為這會阻止類別被實例化

# content of test_pytest_warnings.py
class Test:
    def __init__(self):
        pass

    def test_foo(self):
        assert 1 == 1
$ pytest test_pytest_warnings.py -q

============================= warnings summary =============================
test_pytest_warnings.py:1
  /home/sweet/project/test_pytest_warnings.py:1: PytestCollectionWarning: cannot collect test class 'Test' because it has a __init__ constructor (from: test_pytest_warnings.py)
    class Test:

-- Docs: https://pytest.dev.org.tw/en/stable/how-to/capture-warnings.html
1 warning in 0.12s

可以使用與過濾其他類型警告相同的內建機制來過濾這些警告。

請閱讀我們的 向後相容性政策,以了解我們如何處理棄用並最終移除功能。

完整的警告列表列在 參考文件中

資源警告

如果啟用 tracemalloc 模組,則在 pytest 捕獲 ResourceWarning 時,可以取得有關其來源的額外資訊。

在執行測試時啟用 tracemalloc 的一種便捷方法是將 PYTHONTRACEMALLOC 設定為足夠大的幀數(例如 20,但該數字取決於應用程式)。

有關更多資訊,請參閱 Python 文件中的Python 開發模式部分。