如何捕获警告

How to capture warnings

从版本 3.1 开始,pytest 现在会在测试执行期间自动捕获警告,并在会话结束时显示它们:

# test_show_warnings.py 的内容
import warnings


def api_v1():
    warnings.warn(UserWarning("api v1, 应该使用 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, 应该使用 v2 中的函数
    warnings.warn(UserWarning("api v1, 应该使用 v2 中的函数"))

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

控制警告

Controlling warnings

类似于 Python 的 warning filter-W option 标志,pytest 提供了自己的 -W 标志,用于控制忽略、显示或将哪些警告转变为错误。有关更高级用例,请参见 warning filter 文档。

以下代码示例展示了如何将任何 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, 应该使用 v2 中的函数"))
E       UserWarning: api v1, 应该使用 v2 中的函数

test_show_warnings.py:5: UserWarning
========================= short test summary info ==========================
FAILED test_show_warnings.py::test_one - UserWarning: api v1, 应该使用 ...
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",
    # 注意下面使用单引号表示 TOML 中的“原始”字符串
    'ignore:function ham\(\) is deprecated:DeprecationWarning',
]

当一个警告匹配列表中的多个选项时,将执行最后一个匹配选项的操作。

Note

-W 标志和 filterwarnings ini 选项使用的警告过滤器在结构上相似,但每个配置选项对其过滤器的解释不同。例如,filterwarnings 中的 message 是一个包含正则表达式的字符串,该正则表达式必须不区分大小写地与警告消息的开头匹配,而 -W 中的 message 是一个字面字符串,该字符串必须不区分大小写地包含警告消息的开头(忽略消息开头或结尾的任何空格)。有关更多详细信息,请参阅 warning filter 文档。

@pytest.mark.filterwarnings

``@pytest.mark.filterwarnings``

您可以使用 @pytest.mark.filterwarnings 为特定测试项添加警告过滤器,从而更精细地控制在测试、类或甚至模块级别应捕获哪些警告:

import warnings


def api_v1():
    warnings.warn(UserWarning("api v1, 应该使用 v2 中的函数"))
    return 1


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

使用标记应用的过滤器优先于通过命令行传递或通过 filterwarnings ini 选项配置的过滤器。

您可以通过将 filterwarnings 标记用作类装饰器,来对类中所有测试应用过滤器,或者通过设置 pytestmark 变量,对模块中的所有测试应用过滤器:

# 将该模块中的所有警告转变为错误
pytestmark = pytest.mark.filterwarnings("error")

感谢 Florian Schulze 在 pytest-warnings 插件中的参考实现。

禁用警告摘要

Disabling warnings summary

虽然不推荐,但您可以使用 --disable-warnings 命令行选项完全抑制测试运行输出中的警告摘要。

完全禁用警告捕获

Disabling warning capture entirely

此插件默认启用,但可以通过在 pytest.ini 文件中禁用:

[pytest]
addopts = -p no:warnings

或者在命令行中传递 -p no:warnings。如果您的测试套件使用外部系统处理警告,这可能会很有用。

DeprecationWarning 和 PendingDeprecationWarning

DeprecationWarning and PendingDeprecationWarning

默认情况下,pytest 将显示来自用户代码和第三方库的 DeprecationWarningPendingDeprecationWarning 警告,这符合 PEP 565 的建议。这有助于用户保持代码的现代性,并在弃用警告被有效移除时避免出现故障。

然而,在用户通过 pytest.warns()pytest.deprecated_call() 或使用 recwarn 固件捕获任何类型的警告的特定情况下,将不会显示任何警告。

有时,隐藏一些发生在您无法控制的代码(例如第三方库)中的特定弃用警告是有用的,此时您可以使用警告过滤器选项(ini 或标记)来忽略这些警告。

例如:

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

这将忽略所有类型为 DeprecationWarning 的警告,其中消息的开头与正则表达式 ".*U.*mode is deprecated" 匹配。

有关更多示例,请参见 @pytest.mark.filterwarningsControlling warnings

Note

如果在解释器级别配置了警告,使用 PYTHONWARNINGS 环境变量或 -W 命令行选项,pytest 默认将不配置任何过滤器。

此外,pytest 不遵循 PEP 506 建议重置所有警告过滤器,因为这可能会破坏自行通过调用 warnings.simplefilter() 配置警告过滤器的测试套件(有关此的示例,请参见 #2430)。

确保代码触发弃用警告

Ensuring code triggers a deprecation warning

您还可以使用 pytest.deprecated_call() 来检查某个函数调用是否触发 DeprecationWarningPendingDeprecationWarning

import pytest


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

如果在用 17 参数调用 myfunction 时不发出弃用警告,该测试将失败。

使用 warns 函数断言警告

Asserting warnings with the warns function

你可以使用 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)

# 检查仅引发了一条警告
assert len(record) == 1
# 检查消息是否匹配
assert record[0].message.args[0] == "another warning"

另外,你可以使用 recwarn 夹具详细检查引发的警告(见 below)。

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

记录警告

Recording warnings

你可以使用 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

recwarn 夹具和 pytest.warns() 上下文管理器都返回相同的记录警告的接口:一个 WarningsRecorder 实例。要查看记录的警告,你可以遍历这个实例,调用 len 来获取记录的警告数量,或通过索引获取特定的记录警告。

测试中警告的其他用例

Additional use cases of warnings in tests

以下是一些在测试中经常遇到的与警告相关的用例,以及处理它们的建议:

  • 要确保 至少发出一个 指定的警告,可以使用:

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")
    ...

自定义失败消息

Custom failure messages

记录警告提供了在未发出警告或满足其他条件时生成自定义测试失败消息的机会。

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

如果在调用 f 时没有发出任何警告,则 not record 将评估为 True。然后你可以使用自定义错误消息调用 pytest.fail()

内部 pytest 警告

Internal pytest warnings

在某些情况下,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://docs.pytest.org/en/stable/how-to/capture-warnings.html
1 warning in 0.12s

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

请阅读我们的 向后兼容政策 以了解我们如何处理弃用和最终删除特性。

完整的警告列表在 the reference documentation 中列出。

资源警告

Resource Warnings

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

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

有关更多信息,请参考 Python 文档中的 Python Development Mode 部分。