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

使用 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 支持显示最常见的子表达式的值,包括调用、属性、比较以及二进制和一元运算符。(参见 Demo of Python failure reports with pytest)。这允许您使用惯用的 python 结构而不需要样板代码,同时不会丢失自省信息。

但是,如果你像这样用断言指定一条消息:

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

那么就根本不会发生断言内省,消息将简单地显示在 traceback 中。

有关断言内省的更多信息,请参见 断言内省的细节

关于预期异常的断言

为了编写关于引发的异常的断言,可以使用 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

您可以向上下文管理器传递 match 关键字参数,以测试正则表达式是否与异常的字符串表示相匹配(类似于来自 unittestTestCase.assertRaisesRegex 方法):

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 一起执行,并断言给定的异常被引发:

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

如果出现诸如 no exceptionwrong exception 之类的错误,报告会为您提供有用的输出。

注意,也可以为 pytest.mark.xfail 指定 “raises” 参数,它以一种更具体的方式检查测试是否失败,而不仅仅是抛出任何异常:

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

使用 pytest.raises() 当您在测试自己的代码故意引发的异常时,而使用 @pytest.mark.xfail 可能会更好。可能更适合于记录未修复的 bug(测试描述应该发生什么)或依赖关系中的 bug。”

关于预期警告的断言

您可以使用 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-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

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

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

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

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

如果没有自定义解释,则返回 None,否则返回字符串列表。字符串将由换行符连接,但字符串中的换行符将被转义。注意,除了第一行以外的所有行都将轻微缩进,目的是使第一行成为摘要。

参数:

config (pytest.Config) – pytest 配置对象。

作为一个例子,考虑在 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 中是一个很好的地方)。

For further information, Benjamin Peterson wrote up Behind the scenes of pytest’s new assertion rewriting.

Assertion rewriting caches files on disk

pytest will write back the rewritten modules to disk for caching. You can disable this behavior (for example to avoid leaving stale .pyc files around in projects that move files around a lot) by adding this to the top of your conftest.py file:

import sys

sys.dont_write_bytecode = True

Note that you still get the benefits of assertion introspection, the only change is that the .pyc files won’t be cached on disk.

Additionally, rewriting will silently skip caching if it cannot write new .pyc files, i.e. in a read-only filesystem or a zipfile.

Disabling assert rewriting

pytest rewrites test modules on import by using an import hook to write new pyc files. Most of the time this works transparently. However, if you are working with the import machinery yourself, the import hook may interfere.

If this is the case you have two options:

  • Disable rewriting for a specific module by adding the string PYTEST_DONT_REWRITE to its docstring.

  • Disable rewriting for all modules by using --assert=plain.