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

使用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-7.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 支持显示常见的子表达式的值,包括函数调用、属性访问、比较操作以及二元和一元运算符(请参阅 tbreportdemo)。这允许您在不丢失内省信息的情况下使用Python的惯用构造,而无需编写模板代码。

当然,你也可以像下面所示,指定断言失败的返回消息:

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

这样将不会断言失败对比信息(内省信息),而只简单地在追溯信息中显示你指定的失败返回信息。

有关断言内省的更多信息,请参阅 assert-details

异常断言

你可以像如下所示,使用 pytest.raises 作为上下文管理器来进行异常断言:

Python
import pytest


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

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

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

        def f():
            f()

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

excinfo 是一个 ~pytest.ExceptionInfo 实例,它是实际异常的装饰器。主要属性包括 .type.value.traceback

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

Python
import pytest


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


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

match 方法的 regexp 参数与 re.search 函数匹配,因此在上面的示例中,match='123' 也可以起作用。

pytest.raises 函数有一种备用形式,其中您传递一个函数,该函数将使用给定的 *args**kwargs 执行,并断言是否引发了指定的异常:

Python
pytest.raises(ExpectedException, func, *args, **kwargs)

在失败的情况下,报告器将为您提供有用的输出,例如没有异常错误的异常

请注意,您还可以在 pytest.mark.xfail 中指定一个 "raises" 参数,该参数用于更具体地检查测试是否失败,而不仅仅是引发任何异常:

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

使用 pytest.raises 通常更适用于测试您自己的代码故意引发的异常,而使用带有检查函数的 @pytest.mark.xfail 可能更适用于记录尚未修复的错误(在这种情况下,测试用例描述了应该发生的情况),或者用于检测依赖项中的错误。

警示断言

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

使用上下文对比

Pytest 可以在断言的比较中提供丰富的上下文信息。 例如:

Python
# 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-7.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         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 =============================

对大量用例进行了特定对比:

  • 长字符串断言:显示上下文差异
  • 长序列断言:显示第一个失败的索引
  • 字典断言:显示不同的键值对

有关更多示例,请参阅reporting demo <tbreportdemo>

自定义断言对比信息

可以通过实现 pytest_assertrepr_compare hook 来在断言结果中添加你自己的详细说明信息。

\_pytest.hookspec.pytest_assertrepr_compare

例如,考虑在 conftest.py 文件中添加以下 hook,该 hook 提供了 Foo 对象的替代说明:

Python
# 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:",
            "   vals: {} != {}".format(left.val, right.val),
        ]

在测试模块使用:

Python
# 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.py文件中定义的输出:

$ 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

高级断言内省

将关于失败断言的详细信息报告出来是通过在运行之前重新编写assert语句来实现的。重新编写的assert语句将内省信息放入断言失败消息中。pytest只会重新编写它的测试集合过程直接发现的测试模块,因此不会重新编写支持模块中的assert语句,这些支持模块本身不是测试模块

您可以在导入模块之前调用 register_assert_rewrite <assertion-rewriting> 来手动启用对导入模块的断言重写(比如可以在conftest.py这样使用)。这样可以确保支持模块中的assert语句也会被重写,以提供更详细的断言失败信息。

对于进一步的信息,Benjamin Peterson 撰写了一篇关于 pytest 的新断言重写背后的文章,您可以查看 Behind the scenes of pytest's new assertion rewriting 了解更多详情。这篇文章可能会提供有关pytest断言重写的深入见解。

断言重写在磁盘上缓存文件

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

Python
import sys

sys.dont_write_bytecode = True

请注意,尽管禁用了断言重写缓存,您仍然可以享受断言内省的好处,唯一的变化是不会在磁盘上保留.pyc文件。

此外,如果断言重写无法写入新的.pyc文件,例如在只读文件系统或zip文件中,它会自动跳过缓存操作。

禁用断言重写

pytest通过使用导入 hook 在导入模块时进行重写,生成新的 pyc 文件。大多数情况下,这个过程都会自动进行,不需要额外的干扰。然而,如果您自己在项目中处理模块导入的话,可能会受到 pytest 的导入 hook 的干扰。

如果这种情况发生,您有两个选项:

  • 对于特定模块,您可以通过在其文档字符串中添加字符串PYTEST_DONT_REWRITE来禁用重写。
  • 对于所有模块,您可以使用--assert=plain来禁用重写。

results matching ""

    No results matching ""