如何捕获警告¶
从版本 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.pythonlang.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.pythonlang.cn/en/stable/how-to/capture-warnings.html
1 warning in 0.12s
可以使用与用于过滤其他类型警告的相同内置机制来过滤这些警告。
请阅读我们的 向后兼容性政策 以了解我们如何继续弃用并最终删除功能。
警告的完整列表在 参考文档 中列出。
资源警告¶
如果启用了 tracemalloc
模块,则当 pytest 捕获 ResourceWarning
时,可以获得其来源的其他信息。
在运行测试时启用 tracemalloc
的一种便捷方法是将 PYTHONTRACEMALLOC
设置为足够多的帧(比如 20
,但该数字取决于应用程序)。
有关更多信息,请参阅 Python 文档中的 Python 开发模式 部分。