【python】之pytest在测试中编写和报告断言(三)

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

3.1 使用 assert 语句断言

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

assert 是Python中的关键字,assert 后面跟的如果为Ture 就通过,为False

1
2
3
4
5
6
7
# content of test_assert1.py
def f():
return 3


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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ 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 # 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支持显示最常见的子表达式的值,包括调用、属性、比较、二进制和一元操作符。

如果使用如下断言指定消息:

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

它将会与回溯中的断言检查一起打印。

示例:

1
2
3
def test_study(self):
a = 1+1
assert a == 0, "如果断言失败,就会在AssertionError中打印这句话,如果成功就不会打印"

3.2 关于预期异常的断言

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

1
2
3
4
5
6
import pytest


def test_zero_division():
with pytest.raises(ZeroDivisionError): # pytest.raises 断言解释器报错
1 / 0

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

1
2
3
4
5
6
7
8
9
def test_recursion_depth():
with pytest.raises(RuntimeError) as excinfo:

def f():
f()

f()
assert "maximum recursion" in str(excinfo.value)
excinfo`是一个[`ExceptionInfo`](https://docs.pytest.org/en/8.0.x/reference/reference.html#pytest.ExceptionInfo)实例,它是实际引发的异常的包装器。主要感兴趣的属性 是`.type`和。`.value``.traceback

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def test_foo_not_implemented():
def foo():
raise NotImplementedError

with pytest.raises(RuntimeError) as excinfo:
foo()
assert excinfo.type is RuntimeError # 这是官方文档的,是错的,因为NotImplementedError是RuntimeError的子类 所以不是is 而是换成issubclass
assert issubclass(excinfo.type, RuntimeError) # 这里所执行的foo()抛出的错误是NotImplementedError 所以断言通过了

# 另写一个例子
def foo():
return 1/0 # 运行foo()会抛出一个ZeroDivisionError 0为除数异常
def test_arithmetic_error():
with pytest.raises(Exception) as excinfo: # pytest.raises()可以传Exception,Exception是所有错误的基类,这里也可以传ArithmeticError
foo()
assert issubclass(excinfo.type, ArithmeticError)

即使函数引发 NotImplementedError,pytest.raises() 调用也会成功,因为 NotImplementedErrorRuntimeError 的子类;但是下面的断言语句将捕获该问题。

3.2.1 匹配异常消息

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

1
2
3
4
5
6
7
8
9
10
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' 也可以工作
  • 匹配参数还与 PEP-678 __notes__ 匹配。

3.2.2 匹配例外组exception groups

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

1
2
3
4
5
6
7
8
9
10
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关键字参数;直接包含在top ExceptionGroup中的异常将匹配depth=1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)

3.2.3 xfail mark 和 pytest.raises

也可以为pytest.mark指定一个raise参数。Xfail它以一种更具体的方式检查测试是否失败,而不仅仅是引发任何异常:

1
2
3
4
5
6
7
8
9
10
11
def f():
raise IndexError()


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

testcases/test_case.py x [100%]

============================== 1 xfailed in 0.01s ==============================

如果测试因引发IndexError或子类而失败,也会“xfail”。

  • 使用带有参数的pytest.mark.xfail raises更适合记录未修复的bug(其中可以描述“应该”发生什么)或者依赖项中的错误
  • 大多数情况下,测试你自己故意引发的异常错误的代码的情况,更适合使用pytest.raises()可能会更好一点,

3.3 利用上下文相关的比较

pytest 对在遇到比较时提供上下文相关信息有丰富的支持。例如:

1
2
3
4
5
6
# content of test_assert2.py
def test_set_comparison():
set1 = set("1308")
set2 = set("8035")
assert set1 == set2

运行后报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
================================= 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 =============================

针对一些场景进行了专门的比较:

  • 比较长字符串:显示上下文差异
  • 比较长序列:第一个失败的索引
  • 比较字典:不同的条目

有关更多示例,请参阅报告演示 reporting demo

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

可以通过实现 pytest_assertrepr_compare 钩子来添加您自己的详细解释。—>会在“如何编写钩子函数”里讲

pytest_assertrepr_compare(config, op, left, right)

pytest_assertrepr_compare(config, op, left, right)[source]

Return explanation for comparisons in failing assert expressions.

Return None for no custom explanation, otherwise return a list of strings. The strings will be joined by newlines but any newlines in a string will be escaped. Note that all but the first line will be indented slightly, the intention is for the first line to be a summary.

  • Parameters:

    config (Config) – The pytest config object.op (str) – The operator, e.g. "==", "!=", "not in".left (object) – The left operand.right (object) – The right operand.

1
2
3
4
5
6
7
8
9
10
11
12
13
def pytest_assertrepr_compare(config, op, left, right):
"""
Return explanation for comparisons in failing assert expressions.
返回失败断言表达式中的比较的解释。
Return None for no custom explanation, otherwise return a list of strings. The strings will be joined by newlines but any newlines in a string will be escaped. Note that all but the first line will be indented slightly, the intention is for the first line to be a summary.
如果没有自定义解释,则返回 None,否则返回字符串列表,字符串将通过换行符连接,但字符串中的任何换行符都将被转义,请注意,除了第一行之外的所有行都将稍微缩进,目的是让第一行成为摘要。
Parameters:
config (Config) – The pytest config object. pytest 配置对象
op (str) – The operator, e.g. "==", "!=", "not in".操作员,例如“==”、“!=”、“不在”
left (object) – The left operand. 左操作数。
right (object) – The right operand.右操作数。
"""

作为示例,请考虑在 conftest.py 文件中添加以下挂钩,该文件为 Foo 对象提供替代解释:

1
2
3
4
5
6
7
8
9
10
# 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}",
]

现在,编写这个测试模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 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文件中定义的自定义输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ 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

3.5 Assertion introspection details

不做多介绍了,没啥用


【python】之pytest在测试中编写和报告断言(三)
http://example.com/2024/01/22/663python之pytest在测试中编写和报告断言-三/
作者
Wangxiaowang
发布于
2024年1月22日
许可协议