pytest8.x版本 中文使用文档-------13.如何捕获警告warnings

目录

控制警告

@pytest.mark.filterwarnings

禁用警告摘要

完全禁用警告捕获

DeprecationWarning 和 PendingDeprecationWarning

确保代码触发弃用警告

使用 warns 函数断言警告

记录警告

测试中警告的额外用例

自定义失败消息

内部 pytest 警告


从版本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://docs.pytest.org/en/stable/how-to/capture-warnings.html
======================= 1 passed, 1 warning in 0.12s =======================

控制警告

类似于Python的警告过滤器(warning filter)和-W option选项标志,pytest提供了自己的-W标志来控制哪些警告被忽略、显示或转换为错误。要了解更高级的用法,请参阅警告过滤器的文档 warning filter

下面的代码示例展示了如何将任何UserWarning类别的warning类视为错误:

$ 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命令行工具的-W选项,我们指定了error::UserWarning,这意味着任何UserWarning都将被视为错误。因此,当api_v1函数在test_one测试中被调用并发出UserWarning时,pytest会将其视为测试失败,并显示相应的错误消息。

这有助于确保开发者注意到并处理那些可能表明代码中存在潜在问题的警告。通过将警告视为错误,可以确保测试套件在检测到这些潜在问题时不会通过,从而促使开发者修复这些问题。

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

# pytest.ini
[pytest]
filterwarnings =
    error
    ignore::UserWarning   #pytest -W 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 装饰器允许你为特定的测试项添加警告过滤器,这样你就可以在测试、类或模块级别更精细地控制应该捕获哪些警告。这对于在测试过程中忽略那些预期会出现但不影响测试结果的警告非常有用。

下面是一个使用 @pytest.mark.filterwarnings 的例子:

import warnings  
  
# 假设这是一个旧版本的API函数,它发出一个警告提示用户应该使用v2版本的函数  
def api_v1():  
    warnings.warn(UserWarning("api v1, should use functions from v2"))  
    return 1  
  
# 使用 @pytest.mark.filterwarnings 装饰器来忽略包含特定文本("api v1")的UserWarning  
@pytest.mark.filterwarnings("ignore:api v1")  
def test_one():  
    # 调用 api_v1() 时,尽管它发出了警告,但由于装饰器的存在,该警告将被忽略  
    assert api_v1() == 1

test_one 函数通过 @pytest.mark.filterwarnings("ignore:api v1") 装饰器被标记为忽略所有包含 "api v1" 文本的 UserWarning 警告。因此,当 api_v1() 函数在 test_one 中被调用时,尽管它发出了警告,但该警告不会被 pytest 捕获或显示,测试将正常通过。

使用标记(mark)应用的过滤器会优先于命令行上传递的过滤器或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 会根据 PEP 565.的建议,显示来自用户代码和第三方库的 DeprecationWarning 和 PendingDeprecationWarning 警告。这有助于用户保持代码的现代化,并避免在弃用警告被实际移除时发生中断。通过显示这些警告,pytest 鼓励开发者更新他们的代码,以避免在未来版本中遇到兼容性问题。

然而,在特定情况下,如果用户在测试中捕获了任何类型的警告,无论是使用 pytest.warns()pytest.deprecated_call()还是使用 recwarn夹具(fixture),那么根本不会显示任何警告。

有时,隐藏一些你无法控制的代码(如第三方库)中产生的特定弃用警告是有用的,这时你可以使用警告过滤器选项(在 ini 文件中或作为标记)来忽略这些警告。这样做可以帮助你专注于与你自己的代码或你正在测试的特定部分相关的警告。

例如:

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

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

查看  @pytest.mark.filterwarnings 和 控制警告(Controlling warnings )部分以获取更多示例。

注意

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

此外,pytest 不会遵循PEP 506  的建议来重置所有警告过滤器,因为这可能会破坏那些通过调用 warnings.simplefilter() 来自行配置警告过滤器的测试套件(请参阅 #2430作为此类情况的一个示例)。pytest 允许测试套件保持对警告处理的完全控制,包括在测试期间可能需要的任何特定警告过滤设置。

确保代码触发弃用警告

你还可以使用 pytest.deprecated_call() 来检查某个函数调用是否触发了 DeprecationWarning 或 PendingDeprecationWarning

import pytest  
  
def myfunction(value):  
    # 假设这个函数在某个版本的更新中被弃用  
    warnings.warn("myfunction() is deprecated", DeprecationWarning, stacklevel=2)  
  
def test_myfunction_deprecated():  
    with pytest.deprecated_call():  
        myfunction(17)

这个测试将会失败,如果 myfunction 在使用参数 17 调用时没有发出弃用警告。

pytest.deprecated_call() 上下文管理器会在退出时检查是否捕获到了至少一个弃用警告。如果没有捕获到任何弃用警告,测试将失败。这有助于确保你的代码在预期时触发弃用警告,从而帮助用户或维护者逐步迁移到新的 API 或实践。

使用 warns 函数断言警告

你可以使用pytest.warns() 来检查代码是否抛出了特定的警告,这与使用 raises 的方式类似(但 raises 不会捕获所有异常,只捕获预期的异常类型):

import warnings

import pytest


def test_warning():
    # pytest.warns来捕获并验证在上下文块内部抛出的警告。
    # pytest.warns接受一个或多个警告类作为参数,并期望在上下文块内部抛出这些类型的警告。
    # 如果抛出了预期类型的警告,测试将继续执行;如果没有抛出,或抛出不同类型的警告,测试将失败 
    with pytest.warns(UserWarning):
    # warnings.warn函数生成了一个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"

展示了如何使用pytest.warns()上下文管理器来捕获并验证在代码块中发出的RuntimeWarningpytest.warns()会执行其代码块内的内容,并捕获所有符合指定警告类型的警告。在这个例子中,它捕获了一个RuntimeWarning,并将这些警告作为warnings.WarningMessage对象的列表存储在record变量中。

然后,代码通过断言(assert语句)来验证:

  1. record列表中只有一个警告对象(即len(record) == 1)。
  2. 该警告对象的消息内容(record[0].message.args[0])与预期的"another warning"字符串相匹配

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

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

记录警告

你可以使用pytest.warns() 上下文管理器或者使用recwarn夹具来记录引发的警告。

要使用pytest.warns()来记录警告而不断言任何特定的警告类型,你可以不传递任何参数作为期望的警告类型,此时它将默认为捕获所有通用的Warning

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  
    # 从列表中弹出一个UserWarning类型的警告  
    w = recwarn.pop(UserWarning)  
    # 验证警告的类型  
    assert issubclass(w.category, UserWarning)  
    # 验证警告的消息内容  
    assert str(w.message) == "hello"  
    # 这些属性(filename, lineno)提供了关于警告发生位置的额外信息  
    assert w.filename  
    assert w.lineno

测试函数test_hello通过recwarn参数接收了一个警告记录器。当在测试函数内部发出警告时,这些警告会被recwarn记录。然后,你可以使用recwarn.pop(UserWarning)来从记录中获取特定类型的警告(在这个例子中是UserWarning),并对其进行断言和检查。w.filenamew.lineno等属性提供了有关警告发生位置(即哪个文件和哪一行代码)的额外信息,这对于调试和验证警告的来源非常有用。

recwarnpytest.warns()都为记录的警告返回相同的接口:一个WarningsRecorder实例。要查看记录的警告,你可以遍历这个实例,调用len()函数来获取记录的警告数量,或者使用索引来访问特定的记录警告。

  • pop(category=Warning, msg=None):从记录的警告中移除并返回第一个匹配给定类别(category)和可选消息(msg)的警告。如果未找到匹配的警告,则可能引发KeyError或返回None(具体行为取决于pytest的版本和配置)。
  • list(WarningsRecorder):将WarningsRecorder实例转换为一个列表,其中包含所有记录的警告。

WarningsRecorder的完整API。

测试中警告的额外用例

以下是一些在测试中经常出现的涉及警告的用例,以及如何处理它们的建议:

确保至少发出一个指定的警告

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()  # 假设f是一个可能发出警告的函数  
        if not record:  
            pytest.fail("Expected a warning!")

如果在调用f()时没有发出任何警告,那么not record将评估为True。随后,你可以使用自定义的错误消息调用pytest.fail(),以指示测试失败的原因。在这个例子中,如果f()没有发出任何警告,测试将失败,并显示消息“Expected a warning!”,这有助于测试人员或开发者理解测试失败的原因。

内部 pytest 警告

在某些情况下,pytest 可能会生成自己的警告,比如遇到不当用法或已弃用特性时。

例如,如果 pytest 遇到一个类,该类匹配了 python_classes  配置项,但同时也定义了 __init__ 构造函数,pytest 将发出一个警告,因为这会阻止该类被实例化:

# 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

pytest 遇到了一个名为 Test 的类,该类包含了一个 __init__ 构造函数,这通常意味着它可能是一个需要实例化的类。然而,在 pytest 中,测试类(即包含测试方法的类)通常不需要显式实例化;pytest 会自动发现并调用这些类中的测试方法。

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

请阅读我们的向后兼容性政策Backwards Compatibility Policy ,以了解我们如何逐步弃用并最终移除某些功能。

完整的警告列表可以在参考文档the reference documentation.中找到。

  • 17
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值