自动化测试中如何判断测试是否通过?详解 Pytest 测试框架的断言用法

目录

前言:

[01 --- Python 原生的 assert](#01 — Python 原生的 assert)

[02 --- Pytest 的 assert 优点](#02 — Pytest 的 assert 优点)

[03 --- Pytest 断言的用法](#03 — Pytest 断言的用法)

[4.1 断言字符串](#4.1 断言字符串)

[4.2 断言函数或者接口返回值](#4.2 断言函数或者接口返回值)

[4.3 断言集合类型](#4.3 断言集合类型)

[04 --- Pytest 断言 Excepiton](#04 — Pytest 断言 Excepiton)

[05 --- 为断言添加自定义功能](#05 — 为断言添加自定义功能)

[06 --- 禁止 Pytest 的 assert 特性](#06 — 禁止 Pytest 的 assert 特性)

[07 --- 总结](#07 — 总结)


前言:

在自动化测试中,判断测试是否通过是非常重要的一步。Pytest测试框架提供了丰富的断言用法,用于验证测试结果是否符合预期。

本文将详细介绍 Pytest 的断言,与 TestNG 相比它更加简单,只有一个 assert 语句,但是功能非常强大并且简单易用。

01 --- Python 原生的 assert

Python 中 assert 语句通常用来对代码进行必要的检查,确定某种情况一定发生,或者一定不会发生。

Python 的 assert 语句的语法是这样的:

复制代码
assert expression1 ["," expression2]

expression1 往往是一个条件表达式,如果条件表达式为 True,则什么也不做,相当于执行了 pass 语句;如果条件表达式为 False,便会抛出异常 AssertionError,并返回具体的错误信息 expression2。看一个实际例子:

复制代码
# content of my_assertion.py
def assertion():
    assert [1, 2, 3] == [1, 2, 4], "left is [1,2,3], right is [1,2,4]"
​
if __name__ == '__main__':
    assertion()

执行一下上面的代码看看结果:

复制代码
$ python my_assertion.py 
Traceback (most recent call last):
  File "my_assertion.py", line 5, in <module>
    assertion()
  File "my_assertion.py", line 2, in assertion
    assert [1, 2, 3] == [1, 2, 4], "left is [1,2,3], right is [1,2,4]"
AssertionError: left is [1,2,3], right is [1,2,4]

可见,assert 后面的条件表达式为 False,抛出了 AssertionError,并显示了错误信息 left is [1, 2, 3], right is [1, 2, 4]。

不过,这里还有一点小小的缺憾。并没有明确告诉开发人员,条件判断失败的具体位置。需要开发人员自己对比才发现,==左边的第三个元素和右边的第三个元素不一样。

02 --- Pytest 的 assert 优点

软件测试工作,经常会遇到断言失败的情况。如果每次失败,都需要测试工程师人眼去观察失败的具体原因和出错的位置,那将是非常耗时的。强大的 Pytest 也考虑到了广大测试工程师面临的问题,因此对 Python 原生的 assert 语句进行了优化和改进,主要在是当断言失败时,将错误的具体信息和位置显示出来,让测试工程师对失败原因一目了然。

还是上面的例子,将其放入到测试用例(test_开头的函数)中:

复制代码
# content of test_assertion.py
def test_assertion():
    assert [1, 2, 3] == [1, 2, 4], "left is [1,2,3], right is [1,2,4]"
执行测试用例后的信息输出如下:

    def test_assertion():
>       assert [1, 2, 3] == [1, 2, 4], "left is [1,2,3], right is [1,2,4]"
E       AssertionError: left is [1,2,3], right is [1,2,4]
E       assert [1, 2, 3] == [1, 2, 4]
E         At index 2 diff: 3 != 4
E         Full diff:
E         - [1, 2, 4]
E         ?        ^
E         + [1, 2, 3]
E         ?

是不是有种很爽的感觉?pytest 明确显示出了错误的位置是 index 为 2 的元素左右不相等。这一点点小小的改进大大提高了测试失败时定位出错原因的效率。

在测试用例中执行 assert 语句,才有上面的效果,这是因为 Pytest 对 assert 语句进行了重写。在非测试用例中的 assert 语句,比如测试项目中的一些 utils 函数中,使用 assert 还是 Python 原生的效果。

03 --- Pytest 断言的用法

在自动化测试用例中,最常用的断言是相等断言,就是断言预期结果和实际结果是一致的。通常我们断言的预期结果和实际结果的数据类型是字符串、元组、字典、列表和对象。Pytest 通过 assert 和==能够完美支持对这些数据类型的相等断言。下面来介绍几种常见的数据类型的断言操作。

4.1 断言字符串

断言字符串非常简单,只需要将预期和实际的字符串,分别写在==两边,当发生断言失败时,将会列出第一个不相等元素的下标。下面是几个在实际测试工作中经常用到的几种字符串断言方式。

复制代码
# content of test_assertions.py
class TestAssertions(object):
    def test_string_1(self):
        assert "spam" == "eggs"
​
    def test_string_2(self):
        assert "foo 1 bar" == "foo 2 bar"
​
    def test_string_3(self):
        assert "foo\nspam\nbar" == "foo\neggs\nbar"
​
    def test_string_4(self):
        def f():
            return "streaming"
        assert f().startswith('S')

执行一下这些测试用例,看下输出效果,核心部分如下:

复制代码
============================================================ FAILURES ============================================================
__________________________________________________ TestAssertions.test_string_1 __________________________________________________
​
self = <test_assertions.TestAssertions object at 0x10911a4d0>
​
    def test_string_1(self):
>       assert "spam" == "eggs"
E       AssertionError: assert 'spam' == 'eggs'
E         - eggs
E         + spam
​
tests/test_assertions.py:3: AssertionError
__________________________________________________ TestAssertions.test_string_2 __________________________________________________
​
self = <test_assertions.TestAssertions object at 0x10911a890>
​
    def test_string_2(self):
>       assert "foo 1 bar" == "foo 2 bar"
E       AssertionError: assert 'foo 1 bar' == 'foo 2 bar'
E         - foo 2 bar
E         ?     ^
E         + foo 1 bar
E         ?     ^
​
tests/test_assertions.py:6: AssertionError
__________________________________________________ TestAssertions.test_string_3 __________________________________________________
​
self = <test_assertions.TestAssertions object at 0x10911c2d0>
​
    def test_string_3(self):
>       assert "foo\nspam\nbar" == "foo\neggs\nbar"
E       AssertionError: assert 'foo\nspam\nbar' == 'foo\neggs\nbar'
E           foo
E         - eggs
E         + spam
E           bar
​
tests/test_assertions.py:9: AssertionError
__________________________________________________ TestAssertions.test_string_4 __________________________________________________
​
self = <test_assertions.TestAssertions object at 0x109106a90>
​
    def test_string_4(self):
        def f():
            return "streaming"

>       assert f().startswith('S')
E       AssertionError: assert False
E        +  where False = <built-in method startswith of str object at 0x1090f7bb0>('S')
E        +    where <built-in method startswith of str object at 0x1090f7bb0> = 'streaming'.startswith
E        +      where 'streaming' = <function TestAssertions.test_string_4.<locals>.f at 0x10914b440>()
​
tests/test_assertions.py:15: AssertionError

再次感觉到测试结果一目了然。

4.2 断言函数或者接口返回值

对函数返回值、接口返回值的断言,应该是软件自动化测试中最常见的场景了。这里以函数返回值的断言为例,

复制代码
def test_function():
    def f():
        return [1, 2, 3]
​
    assert f() == [1, 2, 4]

执行这个测试用例,看下输出的错误信息:

复制代码
============================================================ FAILURES ============================================================
_________________________________________________________ test_function __________________________________________________________
​
    def test_function():
        def f():
            return [1, 2, 3]

>       assert f() == [1, 2, 4]
E       assert [1, 2, 3] == [1, 2, 4]
E         At index 2 diff: 3 != 4
E         Full diff:
E         - [1, 2, 4]
E         ?        ^
E         + [1, 2, 3]
E         ?        ^
​
tests/test_assertions.py:22: AssertionError
​

可以看到,输出信息中包含了函数的返回值,并且显示了返回值与预期结果不一致的元素是 index 为 2 的元素。

4.3 断言集合类型

断言列表、元组、字典和集合等类型在测试中也是很常见的,对于具有嵌套的集合数据,pytest 的 assert 依然能够精确地显示出来出错的位置。比如下面这段测试用例代码:

复制代码
class TestCollections(object):
    def test_dict(self):
        assert {"a": 0, "b": 1, "c": 0} == {"a": 0, "b": 2, "d": 0}
​
    def test_dict2(self):
        assert {"a": 0, "b": {"c": 0}} == {"a": 0, "b": {"c": 2}}
​
    def test_list(self):
        assert [0, 1, 2] == [0, 1, 3]
​
    def test_list2(self):
        assert [0, 1, 2] == [0, 1, [1, 2]]
​
    def test_set(self):
        assert {0, 10, 11, 12} == {0, 20, 21}

执行上面的测试代码,核心输出会是下面这样:

复制代码
============================================================ FAILURES ============================================================
___________________________________________________ TestCollections.test_dict ____________________________________________________
​
self = <test_assertions.TestCollections object at 0x10b0d2d10>
​
    def test_dict(self):
>       assert {"a": 0, "b": 1, "c": 0} == {"a": 0, "b": 2, "d": 0}
E       AssertionError: assert {'a': 0, 'b': 1, 'c': 0} == {'a': 0, 'b': 2, 'd': 0}
E         Omitting 1 identical items, use -vv to show
E         Differing items:
E         {'b': 1} != {'b': 2}
E         Left contains 1 more item:
E         {'c': 0}
E         Right contains 1 more item:
E         {'d': 0}...
E         
E         ...Full output truncated (6 lines hidden), use '-vv' to show
​
tests/test_assertions.py:27: AssertionError
___________________________________________________ TestCollections.test_dict2 ___________________________________________________
​
self = <test_assertions.TestCollections object at 0x10b0d2a90>
​
    def test_dict2(self):
>       assert {"a": 0, "b": {"c": 0}} == {"a": 0, "b": {"c": 2}}
E       AssertionError: assert {'a': 0, 'b': {'c': 0}} == {'a': 0, 'b': {'c': 2}}
E         Omitting 1 identical items, use -vv to show
E         Differing items:
E         {'b': {'c': 0}} != {'b': {'c': 2}}
E         Full diff:
E         - {'a': 0, 'b': {'c': 2}}
E         ?                     ^
E         + {'a': 0, 'b': {'c': 0}}...
E         
E         ...Full output truncated (2 lines hidden), use '-vv' to show
​
tests/test_assertions.py:30: AssertionError
___________________________________________________ TestCollections.test_list ____________________________________________________
​
self = <test_assertions.TestCollections object at 0x10b0c1190>
​
    def test_list(self):
>       assert [0, 1, 2] == [0, 1, 3]
E       assert [0, 1, 2] == [0, 1, 3]
E         At index 2 diff: 2 != 3
E         Full diff:
E         - [0, 1, 3]
E         ?        ^
E         + [0, 1, 2]
E         ?        ^
​
tests/test_assertions.py:33: AssertionError
___________________________________________________ TestCollections.test_list2 ___________________________________________________
​
self = <test_assertions.TestCollections object at 0x10b0d6c10>
​
    def test_list2(self):
>       assert [0, 1, 2] == [0, 1, [1, 2]]
E       assert [0, 1, 2] == [0, 1, [1, 2]]
E         At index 2 diff: 2 != [1, 2]
E         Full diff:
E         - [0, 1, [1, 2]]
E         ?        ----  -
E         + [0, 1, 2]
​
tests/test_assertions.py:36: AssertionError
____________________________________________________ TestCollections.test_set ____________________________________________________
​
self = <test_assertions.TestCollections object at 0x10b0c1a50>
​
    def test_set(self):
>       assert {0, 10, 11, 12} == {0, 20, 21}
E       AssertionError: assert {0, 10, 11, 12} == {0, 20, 21}
E         Extra items in the left set:
E         10
E         11
E         12
E         Extra items in the right set:
E         20
E         21...
E         
E         ...Full output truncated (4 lines hidden), use '-vv' to show
​
tests/test_assertions.py:39: AssertionError

可以看到对于嵌套的字典和列表,也能显示出不一致数据的具体位置。对于过长的数据,默认是会被 truncated,可以通过-vv 显示全部信息。

除了相等断言,还可以进行大于、小于、不等于、in/not in 等类型的断言。

对于对象的断言,可以进行对象的类型断言、对象本身的断言。这里就不在一一举例,只要记住断言是使用 assert 语句,使用方法与在 Python 语言中的使用方法完全一致就可以了。

更多断言的例子,大家可以参考 Pytest 的官方文档:https://docs.pytest.org/en/latest/example/reportingdemo.html44 个断言的例子,非常全面,几乎涵盖了所有的相等断言的场景。

04 --- Pytest 断言 Excepiton

除了支持对代码正常运行的结果断言之外,Pytest 也能够对 Exception 和 Warnning 进行断言,来断定某种条件下,一定会出现某种异常或者警告。在功能测试和集成测试中,这两类断言用的不多,这里简单介绍一下。

对于异常的断言,Pytest 的语法是:with pytest.raises(异常类型),可以看下面的这个例子:

复制代码
def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0

这个测试用例断言运算表达式 1 除以 0 会产生 ZeroDivisionError 异常。除了对异常类型进行断言,还可以对异常信息进行断言,比如:

复制代码
def test_zero_division():
    with pytest.raises(ZeroDivisionError) as excinfo:
        1 / 0
    assert 'division by zero' in str(excinfo.value)

这个测试用例,就断言了 excinfo.value 的内容中包含 division by zero 这个字符串,这在需要断言具体的异常信息时非常有用。

对于 Warnning 的断言,其实与 Exception 的断言的用法基本一致。这里就不介绍了,关于更多的 Exception 和 Warnning 的断言可以参考 Pytest 的官方文档How to write and report assertions in tests --- pytest documentation

05 --- 为断言添加自定义功能

通过前面的介绍,感觉 Pytest 的 assert 挺完美了,又简单又清晰。但是在实际的测试工作中,还会遇到一些实际问题,比如在断言时,最好【自动】添加一些日志,避免我们在测试代码中手动加入日志。还有,最好能将断言的信息,【自动】集成到一些测试报告中,比如 Allure 中(关于 Allure 报告大家可以看之前的文章《用 Pytest+Allure 生成漂亮的 HTML 图形化测试报告》)。这样就能避免在每一个测试脚本中手动写很多重复的代码,从而让我们将更多的时间和精力放到编写测试用例上。

有了这样的想法,接下来看看如何实现。

Pytest 中提供了一个 Hook 函数 pytest_assertrepr_compare,这个函数会在测试脚本的 assert 语句执行时被调用。因此,可以实现这个函数,在函数中添加写日志和集成 allure 测试报告代码。

完整的代码如下所示:

复制代码
# content of conftest.py
def pytest_assertrepr_compare(config, op, left, right):
    left_name, right_name = inspect.stack()[7].code_context[0].lstrip().lstrip('assert').rstrip('\n').split(op)
    pytest_output = assertrepr_compare(config, op, left, right)
    logging.debug("{0} is\n {1}".format(left_name, left))
    logging.debug("{0} is\n {1}".format(right_name, right))
    with allure.step("校验结果"):
        allure.attach(str(left), left_name)
        allure.attach(str(right), right_name)
    return pytest_output

通过 inspect 获取调用栈信息,从中得到测试脚本中 assert 语句中 op 操作符两边的字符串名称,在日志和测试报告中会用到。接着执行 assertrepr_compare 输出错误详细信息,这些信息就是在执行断言失败时的输出内容,pytest_assertrepr_compare 函数没有对其做任何修改。接着添加了 debug 日志输出和 allure 测试报告的内容,最后再将 assert 的错误信息返回给调用处。

实现了这个函数后,测试脚本不需要做任何修改,依然是直接使用 assert 进行断言。但是能够自动记录日志和生成 allure 测试报告了。

06 --- 禁止 Pytest 的 assert 特性

如果不想要 Pytest 中的 assert 的效果,而是希望保持 Python 原生的 assert 效果,只需要在执行测试是指定一个选项:

复制代码
--assert=plain

这样所有测试用例中的 assert 都变成了 Python 原生的 assert 效果了,如果只想某一个模块保持 Python 原生的 assert 效果,那么就在对应模块的 docstring 中添加 PYTEST_DONT_REWRITE 字符串就好了,也就是在 py 文件的最上面添加类似下面的 docstring 内容:

复制代码
"""
Disable rewriting for a specific module by adding the string:
PYTEST_DONT_REWRITE
"""

不过,我想应该没有人会这么干,因为 Pytest 的 assert 还是更好用一些。

07 --- 总结

本文对比了 Python 原生的 assert 与 Pytest 中的 assert 的区别,详细介绍了 Pytest 中 assert 的用法,并根据测试工作的实际需求,演示了如何通过 pytest_assertrepr_compare 这个 Hook 函数在断言时增加日志和报告输出。希望对你有帮助。

相关推荐
七夜zippoe4 小时前
CANN Runtime任务描述序列化与持久化源码深度解码
大数据·运维·服务器·cann
盟接之桥4 小时前
盟接之桥说制造:引流品 × 利润品,全球电商平台高效产品组合策略(供讨论)
大数据·linux·服务器·网络·人工智能·制造
Fcy6486 小时前
Linux下 进程(一)(冯诺依曼体系、操作系统、进程基本概念与基本操作)
linux·运维·服务器·进程
袁袁袁袁满6 小时前
Linux怎么查看最新下载的文件
linux·运维·服务器
代码游侠6 小时前
学习笔记——设备树基础
linux·运维·开发语言·单片机·算法
主机哥哥6 小时前
阿里云OpenClaw部署全攻略,五种方案助你快速部署!
服务器·阿里云·负载均衡
Harvey9036 小时前
通过 Helm 部署 Nginx 应用的完整标准化步骤
linux·运维·nginx·k8s
珠海西格电力科技7 小时前
微电网能量平衡理论的实现条件在不同场景下有哪些差异?
运维·服务器·网络·人工智能·云计算·智慧城市
释怀不想释怀7 小时前
Linux环境变量
linux·运维·服务器
zzzsde8 小时前
【Linux】进程(4):进程优先级&&调度队列
linux·运维·服务器