如何捕获警告

从版本 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.cn/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

相同的选项可以使用 filterwarnings ini 选项在 pytest.inipyproject.toml 文件中设置。例如,以下配置将忽略所有用户警告和匹配正则表达式的特定弃用警告,但会将所有其他警告转换为错误。

# 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

根据PEP 565的建议,pytest 默认会显示用户代码和第三方库中的 DeprecationWarningPendingDeprecationWarning 警告。这有助于用户保持其代码的现代化,并避免在弃用警告实际移除时出现故障。

然而,在用户通过 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 565 中重置所有警告过滤器的建议,因为它可能会破坏通过调用 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 fixture 详细检查引发的警告(请参阅下文)。

recwarn fixture 会自动确保在测试结束时重置警告过滤器,因此不会泄露任何全局状态。

记录警告

您可以使用 pytest.warns() 上下文管理器或 recwarn fixture 记录引发的警告。

要使用 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 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.cn/en/stable/how-to/capture-warnings.html
1 warning in 0.12s

这些警告可以使用与过滤其他类型警告相同的内置机制进行过滤。

请阅读我们的向后兼容性策略,了解我们如何处理功能弃用和最终移除。

警告的完整列表列在参考文档中。

资源警告

如果启用了 tracemalloc 模块,则当 pytest 捕获到 ResourceWarning 时,可以获得其来源的额外信息。

在运行测试时启用 tracemalloc 的一种便捷方法是将 PYTHONTRACEMALLOC 设置为足够大的帧数(例如 20,但该数字取决于应用程序)。

有关更多信息,请查阅 Python 文档中的Python 开发模式部分。