三 如何在测试中编写和报告断言 3.1 使用 assert
语句断言 pytest 允许使用标准 Python 断言来验证 Python 测试中的期望和值。例如,可以编写以下内容:
assert 是Python中的关键字,assert 后面跟的如果为Ture 就通过,为False
1 2 3 4 5 6 7 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 ================================= 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.12 s =============================
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 pytestdef test_zero_division (): with pytest.raises(ZeroDivisionError): 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.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 assert issubclass (excinfo.type , RuntimeError) def foo (): return 1 /0 def test_arithmetic_error (): with pytest.raises(Exception) as excinfo: foo() assert issubclass (excinfo.type , ArithmeticError)
即使函数引发 NotImplementedError,pytest.raises() 调用也会成功,因为 NotImplementedError
是 RuntimeError
的子类;但是下面的断言语句将捕获该问题。
3.2.1 匹配异常消息 可以将 match 关键字参数传递给上下文管理器,以测试正则表达式是否与异常的字符串表示形式匹配(类似于 unittest 中的 TestCase.assertRaisesRegex 方法):
1 2 3 4 5 6 7 8 9 10 import pytestdef 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.01 s ==============================
如果测试因引发IndexError
或子类而失败,也会“xfail”。
使用带有参数的pytest.mark.xfail raises
更适合记录未修复的bug(其中可以描述“应该”发生什么)或者依赖项中的错误
大多数情况下,测试你自己故意引发的异常错误的代码的情况,更适合使用pytest.raises()
可能会更好一点,
3.3 利用上下文相关的比较 pytest 对在遇到比较时提供上下文相关信息有丰富的支持。例如:
1 2 3 4 5 6 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.12 s =============================
针对一些场景进行了专门的比较:
比较长字符串:显示上下文差异
比较长序列:第一个失败的索引
比较字典:不同的条目
有关更多示例,请参阅报告演示 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 from test_foocompare import Foodef 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 class Foo : def __init__ (self, val ): self.val = val def __eq__ (self, other ): return self.val == other.valdef 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.12 s
3.5 Assertion introspection details 不做多介绍了,没啥用