如何擷取警告

從版本 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

使用標記套用的篩選器優先於命令列傳遞或由 filterwarnings ini 選項設定的篩選器。

你可以透過將 filterwarnings 標記用作類別裝飾器,或透過設定 pytestmark 變數,將篩選器套用至類別的所有測試或模組的所有測試。

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

感謝 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

這將忽略所有訊息開頭與正規表示式 ".*U.*mode is deprecated" 相符的 DeprecationWarning 類型的警告。

請參閱 @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)

如果 myfunction 在使用 17 參數呼叫時未發出不建議使用的警告,此測試將失敗。

使用 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 固定裝置(請參閱下方)詳細檢查引發的警告。

在測試結束時,recwarn 固定裝置會自動重設警告篩選器,因此不會洩漏任何全域狀態。

記錄警告

您可以使用 pytest.warns()recwarn 固定裝置來記錄引發的警告。

若要使用 pytest.warns() 進行記錄,而沒有對警告進行任何斷言,請將預期的警告類型傳遞為無引數,它將預設為一般警告

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 固定裝置會記錄整個函式的警告

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

recwarnpytest.warns() 都會傳回相同的介面供記錄的警告使用:WarningsRecorder 執行個體。若要檢視記錄的警告,您可以反覆執行此執行個體、呼叫 len 來取得記錄的警告數量,或索引它來取得特定的記錄警告。

完整 API:WarningsRecorder

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

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

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

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__ 建構函式,它會發出警告,因為這會阻止類別被實例化

# 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

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

請閱讀我們的 向後相容性政策 以了解我們如何進行功能的棄用和最終移除。

警告的完整清單列於 參考文件 中。

資源警告

如果啟用了 ResourceWarning 模組,則當 pytest 擷取到 tracemalloc 的來源時,可以取得額外的資訊。

在執行測試時啟用 tracemalloc 的一個方便方法是將 PYTHONTRACEMALLOC 設定為足夠數量的框架(例如 20,但這個數字取決於應用程式)。

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