如何捕获警告¶
从版本 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.ini 或 pyproject.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 中的 消息是一个包含正则表达式的字符串,警告消息的开头必须与之匹配(不区分大小写),而 -W 中的 消息是一个文字字符串,警告消息的开头必须包含它(不区分大小写),忽略消息开头或结尾的任何空格。有关更多详细信息,请查阅 警告过滤器 文档。
@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 变量将过滤器应用到类的所有测试,或通过设置 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。如果你的测试套件使用外部系统处理警告,这可能很有用。
弃用警告和待弃用警告¶
默认情况下,pytest 将显示用户代码和第三方库中的 DeprecationWarning 和 PendingDeprecationWarning 警告,如 PEP 565 所建议。这有助于用户保持代码的现代性,并在有效删除弃用警告时避免中断。
但是,在用户在测试中捕获任何类型的警告的特定情况下,无论是使用 pytest.warns()、pytest.deprecated_call() 还是使用 recwarn 固件,都不会显示任何警告。
有时,隐藏一些特定弃用警告很有用,这些警告发生在你无法控制的代码(例如第三方库)中,在这种情况下,你可以使用警告过滤器选项(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() 检查某个函数调用是否触发 DeprecationWarning 或 PendingDeprecationWarning
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 固定装置详细检查已引发的警告(见下文)。
在测试结束时,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
recwarn 和 pytest.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.cn/en/stable/how-to/capture-warnings.html
1 warning in 0.12s
可以使用与用于过滤其他类型警告的相同内置机制来过滤这些警告。
请阅读我们的 向后兼容性政策 以了解我们如何继续弃用并最终删除功能。
警告的完整列表在 参考文档 中列出。
资源警告¶
如果启用了 tracemalloc 模块,则当 pytest 捕获 ResourceWarning 时,可以获得其来源的其他信息。
在运行测试时启用 tracemalloc 的一种便捷方法是将 PYTHONTRACEMALLOC 设置为足够多的帧(比如 20,但该数字取决于应用程序)。
有关更多信息,请参阅 Python 文档中的 Python 开发模式 部分。