如何在测试中编写和报告断言
使用assert
语句进行断言
pytest
允许你使用标准的Python assert
断言语句来验证测试中的期望结果和实际结果。 例如,你可以编写以下内容:
# 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的惯用构造,而无需编写模板代码。
当然,你也可以像下面所示,指定断言失败的返回消息:
assert a % 2 == 0, "value was odd, should be even"
这样将不会断言失败对比信息(内省信息),而只简单地在追溯信息中显示你指定的失败返回信息。
有关断言内省的更多信息,请参阅 assert-details
。
异常断言
你可以像如下所示,使用 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)
excinfo
是一个 ~pytest.ExceptionInfo
实例,它是实际异常的装饰器。主要属性包括 .type
、.value
和 .traceback
。
您可以传递一个 match
关键字参数给上下文管理器,以测试正则表达式是否匹配异常的字符串表示形式(类似于 unittest
中的 TestCase.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)
在失败的情况下,报告器将为您提供有用的输出,例如没有异常或错误的异常。
请注意,您还可以在 pytest.mark.xfail
中指定一个 "raises" 参数,该参数用于更具体地检查测试是否失败,而不仅仅是引发任何异常:
@pytest.mark.xfail(raises=IndexError)
def test_f():
f()
使用 pytest.raises
通常更适用于测试您自己的代码故意引发的异常,而使用带有检查函数的 @pytest.mark.xfail
可能更适用于记录尚未修复的错误(在这种情况下,测试用例描述了应该发生的情况),或者用于检测依赖项中的错误。
警示断言
你可以使用 pytest.warns <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 <tbreportdemo>
。
自定义断言对比信息
可以通过实现 pytest_assertrepr_compare
hook 来在断言结果中添加你自己的详细说明信息。
例如,考虑在 conftest.py
文件中添加以下 hook,该 hook 提供了 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:",
" vals: {} != {}".format(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.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
文件):
import sys
sys.dont_write_bytecode = True
请注意,尽管禁用了断言重写缓存,您仍然可以享受断言内省的好处,唯一的变化是不会在磁盘上保留.pyc文件。
此外,如果断言重写无法写入新的.pyc
文件,例如在只读文件系统或zip文件中,它会自动跳过缓存操作。
禁用断言重写
pytest
通过使用导入 hook 在导入模块时进行重写,生成新的 pyc
文件。大多数情况下,这个过程都会自动进行,不需要额外的干扰。然而,如果您自己在项目中处理模块导入的话,可能会受到 pytest
的导入 hook 的干扰。
如果这种情况发生,您有两个选项:
- 对于特定模块,您可以通过在其文档字符串中添加字符串
PYTEST_DONT_REWRITE
来禁用重写。 - 对于所有模块,您可以使用
--assert=plain
来禁用重写。