如何编写和报告测试中的断言¶
使用 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)
excinfo
是 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)
如果出现诸如 no exception 或 wrong 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
.