如何在测试中编写和报告断言

使用 assert 语句进行断言

pytest 允许您使用标准 Python assert 来验证 Python 测试中的预期和值。例如,您可以编写以下内容

# content of test_assert1.py
def f():
    return 3


def test_function():
    assert f() == 4

以断言您的函数返回某个值。如果此断言失败,您将看到函数调用的返回值

$ pytest test_assert1.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_assert1.py F                                                    [100%]

================================= FAILURES =================================
______________________________ test_function _______________________________

    def test_function():
>       assert f() == 4
E       assert 3 == 4
E        +  where 3 = f()

test_assert1.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_assert1.py::test_function - assert 3 == 4
============================ 1 failed in 0.12s =============================

pytest 支持显示最常见子表达式的值,包括调用、属性、比较以及二元和一元运算符。(请参阅 使用 pytest 演示 Python 失败报告)。这允许您使用惯用的 python 构造,而无需样板代码,同时不会丢失自省信息。

如果使用如下方式为断言指定消息

assert a % 2 == 0, "value was odd, should be even"

它将与回溯中的断言自省一起打印。

有关断言自省的更多信息,请参阅 断言自省详细信息

关于预期异常的断言

为了编写关于引发异常的断言,您可以使用 pytest.raises() 作为上下文管理器,如下所示

import pytest


def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0

如果您需要访问实际异常信息,可以使用

def test_recursion_depth():
    with pytest.raises(RuntimeError) as excinfo:

        def f():
            f()

        f()
    assert "maximum recursion" in str(excinfo.value)

excinfoExceptionInfo 实例,它是对实际引发异常的包装。主要关注的属性是 .type.value.traceback

请注意,pytest.raises 将匹配异常类型或任何子类(如标准 except 语句)。如果您想检查代码块是否引发确切的异常类型,则需要显式检查

def test_foo_not_implemented():
    def foo():
        raise NotImplementedError

    with pytest.raises(RuntimeError) as excinfo:
        foo()
    assert excinfo.type is RuntimeError

即使函数引发 NotImplementedErrorpytest.raises() 调用也会成功,因为 NotImplementedErrorRuntimeError 的子类;但是,以下 assert 语句将捕获问题。

匹配异常消息

你可以将 match 关键字参数传递给上下文管理器,以测试正则表达式是否与异常的字符串表示形式匹配(类似于 TestCase.assertRaisesRegex 方法,来自 unittest

import pytest


def myfunc():
    raise ValueError("Exception 123 raised")


def test_match():
    with pytest.raises(ValueError, match=r".* 123 .*"):
        myfunc()

注意

  • match 参数与 re.search() 函数匹配,因此在上面的示例中 match='123' 也适用。

  • match 参数还与 PEP-678 __notes__ 匹配。

匹配异常组

你还可以使用 excinfo.group_contains() 方法来测试作为 ExceptionGroup 一部分返回的异常

def test_exception_in_group():
    with pytest.raises(ExceptionGroup) as excinfo:
        raise ExceptionGroup(
            "Group message",
            [
                RuntimeError("Exception 123 raised"),
            ],
        )
    assert excinfo.group_contains(RuntimeError, match=r".* 123 .*")
    assert not excinfo.group_contains(TypeError)

可选的 match 关键字参数的工作方式与 pytest.raises() 相同。

默认情况下,group_contains() 将递归搜索嵌套 ExceptionGroup 实例任何级别的匹配异常。如果你只想匹配特定级别的异常,你可以指定一个 depth 关键字参数;直接包含在顶级 ExceptionGroup 中的异常将匹配 depth=1

def test_exception_in_group_at_given_depth():
    with pytest.raises(ExceptionGroup) as excinfo:
        raise ExceptionGroup(
            "Group message",
            [
                RuntimeError(),
                ExceptionGroup(
                    "Nested group",
                    [
                        TypeError(),
                    ],
                ),
            ],
        )
    assert excinfo.group_contains(RuntimeError, depth=1)
    assert excinfo.group_contains(TypeError, depth=2)
    assert not excinfo.group_contains(RuntimeError, depth=2)
    assert not excinfo.group_contains(TypeError, depth=1)

备用形式(旧版)

有一种备用形式,你可以传递一个将被执行的函数,以及 *args**kwargspytest.raises() 将使用参数执行函数,并断言给定的异常被引发

def func(x):
    if x <= 0:
        raise ValueError("x needs to be larger than zero")


pytest.raises(ValueError, func, x=-1)

在发生没有异常错误异常等故障时,报告器将为你提供有用的输出。

此形式是原始 pytest.raises() API,在 with 语句添加到 Python 语言之前开发。如今,这种形式很少使用,使用上下文管理器形式(使用 with)被认为更具可读性。尽管如此,此形式得到充分支持,并且不会以任何方式弃用。

xfail 标记和 pytest.raises

还可以为 pytest.mark.xfail 指定 raises 参数,它检查测试是否以比仅仅引发任何异常更具体的方式失败

def f():
    raise IndexError()


@pytest.mark.xfail(raises=IndexError)
def test_f():
    f()

只有当测试通过引发 IndexError 或其子类失败时,才会“xfail”。

  • pytest.mark.xfailraises 参数一起使用可能更适合于记录未修复的错误(测试描述了“应该”发生的情况)或依赖项中的错误。

  • pytest.raises() 用于测试你自己的代码故意引发的异常的情况可能是更好的选择,这是大多数情况。

关于预期警告的断言

您可以使用 pytest.warns 检查代码是否引发特定警告。

利用上下文敏感比较

pytest 在遇到比较时具有提供上下文敏感信息的丰富支持。例如

# content of test_assert2.py
def test_set_comparison():
    set1 = set("1308")
    set2 = set("8035")
    assert set1 == set2

如果您运行此模块

$ pytest test_assert2.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_assert2.py F                                                    [100%]

================================= FAILURES =================================
___________________________ test_set_comparison ____________________________

    def test_set_comparison():
        set1 = set("1308")
        set2 = set("8035")
>       assert set1 == set2
E       AssertionError: assert {'0', '1', '3', '8'} == {'0', '3', '5', '8'}
E
E         Extra items in the left set:
E         '1'
E         Extra items in the right set:
E         '5'
E         Use -v to get more diff

test_assert2.py:4: AssertionError
========================= short test summary info ==========================
FAILED test_assert2.py::test_set_comparison - AssertionError: assert {'0'...
============================ 1 failed in 0.12s =============================

对许多情况执行特殊比较

  • 比较长字符串:显示上下文差异

  • 比较长序列:第一个失败索引

  • 比较字典:不同的条目

请参阅 报告演示 了解更多示例。

为失败断言定义您自己的解释

通过实现 pytest_assertrepr_compare 钩子,可以添加您自己的详细解释。

pytest_assertrepr_compare(config, op, left, right)[源代码]

返回失败断言表达式中比较的解释。

返回 None 表示没有自定义解释,否则返回字符串列表。字符串将通过换行符连接,但字符串的任何换行符都将被转义。请注意,除了第一行外,所有行都会稍微缩进,目的是让第一行成为摘要。

参数:
  • config (Config) – pytest 配置对象。

  • op (str) – 运算符,例如 "==""!=""not in"

  • left (object) – 左操作数。

  • right (object) – 右操作数。

在 conftest 插件中使用

任何 conftest 文件都可以实现此钩子。对于给定的项目,只会查询项目目录及其父目录中的 conftest 文件。

例如,考虑在 conftest.py 文件中添加以下钩子,该文件为 Foo 对象提供了替代解释

# content of conftest.py
from test_foocompare import Foo


def pytest_assertrepr_compare(op, left, right):
    if isinstance(left, Foo) and isinstance(right, Foo) and op == "==":
        return [
            "Comparing Foo instances:",
            f"   vals: {left.val} != {right.val}",
        ]

现在,给定此测试模块

# content of test_foocompare.py
class Foo:
    def __init__(self, val):
        self.val = val

    def __eq__(self, other):
        return self.val == other.val


def test_compare():
    f1 = Foo(1)
    f2 = Foo(2)
    assert f1 == f2

您可以运行测试模块并获取在 conftest 文件中定义的自定义输出

$ pytest -q test_foocompare.py
F                                                                    [100%]
================================= FAILURES =================================
_______________________________ test_compare _______________________________

    def test_compare():
        f1 = Foo(1)
        f2 = Foo(2)
>       assert f1 == f2
E       assert Comparing Foo instances:
E            vals: 1 != 2

test_foocompare.py:12: AssertionError
========================= short test summary info ==========================
FAILED test_foocompare.py::test_compare - assert Comparing Foo instances:
1 failed in 0.12s

断言内省详细信息

通过在运行断言语句之前重写它们来报告有关失败断言的详细信息。重写的断言语句将内省信息放入断言失败消息中。 pytest 仅重写其测试收集过程直接发现的测试模块,因此不会重写本身不是测试模块的支持模块中的断言

您可以在导入模块之前通过调用 register_assert_rewrite 手动为导入的模块启用断言重写(一个好地方是放在根 conftest.py 中)。

有关更多信息,Benjamin Peterson 撰写了 Pytest 新断言重写的幕后

断言重写将文件缓存到磁盘

pytest 将重写的模块写回磁盘以进行缓存。您可以禁用此行为(例如,为了避免在频繁移动文件的项目中留下过时的 .pyc 文件),方法是将此内容添加到 conftest.py 文件的顶部

import sys

sys.dont_write_bytecode = True

请注意,您仍然可以获得断言自省的好处,唯一的变化是 .pyc 文件不会被缓存到磁盘。

此外,如果无法写入新的 .pyc 文件,重写将静默跳过缓存,即在只读文件系统或 zip 文件中。

禁用断言重写

pytest 通过使用导入挂钩来重写导入时的测试模块,以写入新的 pyc 文件。大多数时候,这都是透明的。但是,如果您自己使用导入机制,导入挂钩可能会干扰。

如果是这种情况,您有两个选择

  • 通过将字符串 PYTEST_DONT_REWRITE 添加到其文档字符串中,为特定模块禁用重写。

  • 通过使用 --assert=plain 为所有模块禁用重写。