测试中断言的编写和报告

4.1 使用assert声明来断言

pytest允许你在测试中使用标准的assert关键字来验证期望值和实际结果。例如你可以编写下面的代码:

# 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-6.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
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 显示实际值已经支持了包括 调用,属性,比较运算,二进制运算,一元运算 在内的最基本的的子表达。(之前的错误报告一节中就有体现 )这种特性允许我们使用最惯用的python结构而不用写模板代码,还不会丢失内部信息。
然而,如果你自定义了失败回显信息,像下面这样:

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

内置的回显信息会被自定义信息替换,自定义的信息会被显示出来。

在 Assertion introspection details(内置断言的细节)这一节中我们将看到更多有关于断言的信息。
4.2 预研期望的异常
为了为引发的异常编写断言,你可以使用 pytest.raise() 作为 context manager(上下文管理器),像下面这样:
译者注:context manager 就是with后面跟着的对象,想要了解更多可以看这个文章链接

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是一个异常信息对象,它是一个真实引发的异常的包装。我们比较关心的最主要的属性是:.type .value .traceback
你可以给上下文管理器传递一个关键字匹配参数,去测试异常的字符串表示是不是符合正则表达式(与unittest的TestCase.assertRaisesRegexp方法类似)。

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() 的调用形式:你传递一个function,他会使用给定的 *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 加一个测试函数的方法更适用于记录为解决的BUG(记录就应该出现的BUG)或依赖库的BUG。

译者注:我们可以从字面意思和最终运行结果加深对于pytest.raises() 和 @pytest.mark.xfail的理解,前者判断某个异常有没有引发,引发了这个测试就通过了,那字面意思其实更强调“这里引发了这个异常就对了”,后者如果代码中引发了指定的异常,结果是xfailed,没有引发异常,结果是xpassed,它的意思更像是“这里有毛病,我要记录一下”

4.3 对于期望的警告的断言

你可以使用 pytest.warns 来测试引发特定警告的情况。
译者注:pytest.warns的使用在之后会讲到,下面放一个例子来提前了解一下,该代码并不在手册中

import warnings
import pytest


def test_warning():
	with pytest.warns(UserWarning):
		warnings.warn("my warning", UserWarning)

4.4 上下文敏感的比较的使用

当遇到比较的时候,pytest能够提供丰富的上下文敏感的信息,比如:
译者注:“上下文敏感的比较”的意思是当我们断言两个元素相等的时候,pytest会根据上下文来看如何比较,即 assert a == b 的时候,pytest会根据a和b的类型来智能的判断

# test_assert2.py
def test_set_comparison():
	set1 = set("1308")
	set2 = set("8035")
	assert set1 == set2

译者注:这段代码体现了两个set的比较
如果你运行这段代码:

$ pytest test_assert2.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
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 the full diff
test_assert2.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_assert2.py::test_set_comparison - AssertionError: assert {'0'...
============================ 1 failed in 0.12s =============================

有一些情况的比较比较特殊:

  • 比较长字符串:显示上下文差异
  • 比较长序列:第一次失败的索引(译者注:python的序列(sequence)有tuple和list两种
  • 比较字典:不同的条目
    要看到更多例子,看测试报告一节的demo。
    译者注:上面告诉了我们这几个不同的类型比较起来的逻辑是怎样的,字符串的比较就是比较两个字符串是不是一模一样,序列的比较,会告诉你不相同的元素索引是多少,字典的比较,会告诉你那个元素不一样,字典跟索引就没关系了,下面给出一个例子,这个例子并不在手册中,是译者为了方便大家理解上面的描述而写的
def test_compare_1():
    assert '1234' == '1235'


def test_compare_2():
    assert [1, 2, 3] == [1, 2, 4]


def test_compare_3():
    assert ('123', 223) == ('23', 223)


def test_compare_4():
    assert {'1': '123', '2': '223'} == {'1': '123', '2': '224'}

运行结果:

C:\Users\xx\testcase>pytest test_compare.py
============================================================== test session starts ===============================================================
platform win32 -- Python 3.7.7, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
rootdir: C:\Users\WuHao\Desktop\1
collected 4 items                                                                                                                                 

test_compare.py FFFF                                                                                                                        [100%]

==================================================================== FAILURES ====================================================================
_________________________________________________________________ test_compare_1 _________________________________________________________________

    def test_compare_1():
>       assert '1234' == '1235'
E       AssertionError: assert '1234' == '1235'
E         - 1235
E         ?    ^
E         + 1234
E         ?    ^

test_compare.py:2: AssertionError
_________________________________________________________________ test_compare_2 _________________________________________________________________

    def test_compare_2():
>       assert [1, 2, 3] == [1, 2, 4]
E       assert [1, 2, 3] == [1, 2, 4]
E         At index 2 diff: 3 != 4
E         Use -v to get the full diff

test_compare.py:6: AssertionError
_________________________________________________________________ test_compare_3 _________________________________________________________________

    def test_compare_3():
>       assert ('123', 223) == ('23', 223)
E       AssertionError: assert ('123', 223) == ('23', 223)
E         At index 0 diff: '123' != '23'
E         Use -v to get the full diff

test_compare.py:10: AssertionError
_________________________________________________________________ test_compare_4 _________________________________________________________________

    def test_compare_4():
>       assert {'1': '123', '2': '223'} == {'1': '123', '2': '224'}
E       AssertionError: assert {'1': '123', '2': '223'} == {'1': '123', '2': '224'}
E         Omitting 1 identical items, use -vv to show
E         Differing items:
E         {'2': '223'} != {'2': '224'}
E         Use -v to get the full diff

test_compare.py:14: AssertionError
============================================================ short test summary info =============================================================
FAILED test_compare.py::test_compare_1 - AssertionError: assert '1234' == '1235'
FAILED test_compare.py::test_compare_2 - assert [1, 2, 3] == [1, 2, 4]
FAILED test_compare.py::test_compare_3 - AssertionError: assert ('123', 223) == ('23', 223)
FAILED test_compare.py::test_compare_4 - AssertionError: assert {'1': '123', '2': '223'} == {'1': '123', '2': '224'}
=============================================================== 4 failed in 0.23s ================================================================

4.5 定义你自己的失败断言说明

通过实现 pytest_assertrepr_compare 钩子,我们可以添加我们自己的说明细节。

pytest_assertrepr_compare(config: Config, op: str, left: object, right: object) → Optional[List[str]]

在测试失败的表达式中返回比较的解释

在没有自定义说明的时候返回None,否则返回一个字符串列表。字符串会被换行符连接,如果字符串中有换行符,将会被转义。注意除了第一行其他行会被轻微的缩进,这样做的目的是让第一行成为一个摘要。

它的参数 config (_pytest.config.Config) 是 pytest 的config对象

可以考虑在conftest.py中添加下面的钩子,来给Foo对象提供一个自定义的解释:

# 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),
	]

然后,使用下面的测试模块:

# 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

4.6 内置的断言细节

通过在assert语句运行之前重写它们,可以报告有关失败断言的详细信息。重写assert可以把内置的信息放进断言失败信息中去。pytest只会重写那些由它的测试用例发现程序直接发现的测试用例,所以模块如果不是可以被发现的测试程序则不会被重写。
通过在导入模块之前调用register_assert_rewrite,可以手动启用导入模块的断言重写(在根目录contest .py中是一个很好的位置)。
更多信息,可以参考Benjamin Peterson 写的 Behind the scenes of pytest’s new assertion rewriting

4.6.1 磁盘上缓存文件的断言重写

pytest将把重写后的模块写回磁盘以进行缓存。你可以把下面的代码添加到你的contest .py文件的顶部来禁用这个行为(例如避免在项目中留下陈旧的.pyc文件,这些文件会移动很多地方):

import sys
sys.dont_write_bytecode = True

注意,您仍然可以使用内置断言,唯一的变化是.pyc文件不会缓存到磁盘上。
此外,如果不能写入新的.pyc文件(即在只读文件系统或zip文件中),重写将静默地跳过缓存。

4.6.2 禁用断言重写

pytest通过使用import钩子来编写新的pyc文件,从而在导入时重写测试模块。这对使用者是透明的,大多数情况下都可行。但在某些情况下,断言重写可能会干扰你。
如果是这种情况你由两个选择:

  • 通过在文档字符串中添加字符串PYTEST_DONT_REWRITE来禁用对特定模块的重写。
  • 使用 --assert=plain禁用所有模块的断言重写

译者注:4.6可能让你摸不着头脑,这里简单解释一下:我们之前讲的,我们可以通过实现钩子,给断言失败加一些解释信息。而python本身就有assert,而标准的assert并不能提供这种功能,之所以有,是因为pytest重写了assert,实现了一些之前没有的功能,关于模块,缓存,禁用重写这些,如果不懂也没关系,后面还会讲到这些内容