写点什么

软件测试 | pytest 测试框架

  • 2023-03-03
    北京
  • 本文字数:9585 字

    阅读完需:约 31 分钟

简介

pytest 是一个全功能的 Python 测试工具,可以帮助您编写更好的程序。它与 Python 自带的 unittest 测 试框架类似,但 pytest 使用起来更简洁和高效,并且兼容 unittest 框架。pytest 支持简单的单元测试和 复杂的功能测试,可以结合 requests 实现接口测试,结合 selenium、appium 实现自动化功能测试,使 用 pytest 结合 Allure2 集成到 Jenkins 中可以实现持续集成。pytest 支持三百多种插件(可以访问网 址:http://plugincompat.herokuapp.com/ 查看插件),可以访问网址:https://docs.pytest.org/ 查看帮助文 档。

安装

pip install -U pytest
复制代码

查看版本

pytest --version
复制代码

用例的识别与运行

用例编写规范

  • 测试文件以 test_开头或以 _test 结尾

  • 测试类以 Test 开头,并且不能带有 __init__ 方法

  • 测试函数以 test_开头

  • 断言使用基本的 assert 即可

创建一个 python 文件,命名以 test_ 开头(或者以 _test 结尾),创建测试方法以 test_ 开 头,测试类需要以 Test 开头。

创建文件名为 test_add.py 文件,代码如下:

#!/usr/bin/env python# -*- coding: utf-8 -*-def add(x, y): return x + ydef test_add(): assert add(1, 10) == 11 assert add(1, 1) == 2 assert add(1, 99) == 100class TestClass: def test_one(self): x = "this"assert "h" in x def test_two(self): x = "hello" assert hasattr(x, "check")
复制代码

命令行进入到这个文件所在的路径,运行 test_add.py 文件。

可以直接使用 pytest 命令运行,pytest 会找当前目录以及递归查找子目录下所有的 test_*.py 或 *_test.py 的文件,把其当作测试文件。在这些文件里,pytest 会收集符合编写规范的函数、类以及 方法,当作测试用例并且执行。

执行如下:

 $ pytest....test_add.py ..F [100%]....self = <test_cases.test_add.TestClass object at 0x1091810d0> def test_two(self): x = "hello"> assert hasattr(x, "check")E AssertionError: assert FalseE + where False = hasattr('hello', 'check')test_add.py:18: AssertionError===================================================== 1 failed, 2 passed in 0.05s...
复制代码

结果分析:

执行结果中, F 代表用例未通过(断言错误), . 代表用例通过。如果报错会有详细的错误信息。 pytest 也支持运行 unittest 模式的用例。

运行参数

pytest 提供了很多参数,可以使用 pytest --help 来查看帮助文档,下面介绍几种常用的参数:

  • -v 参数

打印详细运行日志信息,一般在调试的时候加上这个参数,终端会打印出每条用例的详细日志信息,方 便定位问题。使用方法如下:

pytest -v
复制代码
  • -s 参数

控制台输出结果,当你的代码里面有 print 输出语句,如果想在运行结果中打印 print 输出的代码 (默认控制台是不输出打印结果的),在运行的时候可以添加 -s 参数,一般在调试的时候使用,使 用方法如下:

pytest -s
复制代码
  • -k 参数

只执行含有某个关键字的测试用例。

控制台输出结果,当你的代码里面有 print 输出语句,如果想在运行结果中打印 print 输出的代码 (默认控制台是不输出打印结果的),在运行的时候可以添加 -s 参数,一般在调试的时候使用,使 用方法如下:

pytest -k "类名"pytest -k "方法名"pytest -k "类名 and not 方法名" #运行类里所有的方法,不包含某个方法
复制代码

注意:如果是 windows 系统, -k 后面的字符串参数必须用双引号。

  • -x 参数

遇到用例失败立即停止运行。

应用场景:

在回归测试过程中,假如一共有 10 条基础用例,当开发人员打完包提交测试的时候,需要先运行这 10 条基础用例,全部通过才能提交给测试人员正式测试。如果有一条用例失败,就将这个版本打回给 开发人员。这时就可以添加 -x 参数,一旦发现有失败的用例即中止运行。

使用方法如下:

pytest -x
复制代码
  • --maxfail 参数

用例失败个数达到阈值停止运行。 应用场景: 在回归测试过程中,假如一共有 10 条基础用例,当开发人员打完包提交测试的时候,需要先运行这 10 条基础用例,全部通过才能提交给测试人员正式测试。如果运行过程中有 [num] 条用例失败,即 中止运行,后面测试用例都放弃执行,直接退出。这时可以使用 --maxfail 参数。

具体用法:

pytest --maxfail=[num]
复制代码
  • -m 参数

将运行有 @pytest.mark.[标记名] 这个标记的测试用例。

应用场景:

在自动化测试过程中可以将测试用例添加标签进行分类,比如登录功能、搜索功能、购物车功能、订单 结算功能等,在运行的时候可以只运行某个功能的所有的测试用例,比如这个版本只想验证登录功能, 那就在所有登录功能的测试用例方法上面加上装饰符 @pytest.mark.login ,运行的时候使用命令添 加一个 -m 参数,例如执行 pytest -m login 命令就可以只执行登录功能这部分的测试用例。

使用方法如下:

pytest -m[标记名]
复制代码

运行模式

pytest 提供了多种运行模式,让开发和调试更得心应手。指定某个模块,执行单独一个 pytest 模块。

应用场景:

在编写测试用例的时候,经常会单独调试某个类,或者某个方法,这时可以使用 Pycharm 里面自带的调 试方式,点击用例方法名前面的绿色按钮,也可以使用命令行的方式单独运行某个用例。

pytest 中可以使用 pytest 文件名.py 单独执行某个 python 文件,也可以使用 pytest 文件名.py:: 类名 单独执行某个文件中的类,使用 pytest 文件名.py::类名::方法名 单独执行类中的某个方法。 使用方法如下:

pytest 文件名.pypytest 文件名.py::类名pytest 文件名.py::类名::方法名
复制代码

在 Pycharm 中运行 pytest 用例

打开 Pycharm -> 设置 - Tools -> Python Integrated Tools -> Testing: pytest

首先设置成 pytest ,需要安装 pytest,可以直接按照这个页面的提示点击 fix ,也可以在 Project interpreter 里面添加 pytest 安装包。安装完 pytest 之后,符合规则的测试用例都能被识别出来并且会显 示出一个三角形的执行按钮,点击这个按钮也能执行某个方法或者某个类。例如:

pytest 框架结构

执行用例前后会执行 setup,teardown 来完成用例的前置和后置条件。pytest 框架中使用 setup, teardown 更灵活,按照用例运行级别可以分为以下几类:

模块级(setup_module/teardown_module)在模块始末调用

函数级(setup_function/teardown_function)在函数始末调用(在类外部)

类级(setup_class/teardown_class)在类始末调用(在类中)

方法级(setup_method/teardown_methond)在方法始末调用(在类中)

方法级(setup/teardown)在方法始末调用(在类中)

调用顺序

setup_module > setup_class >setup_method > setup > teardown > teardown_method > teardown_class > teardown_module

画图说明

验证上面的执行顺序,看下面的案例。

创建文件名为 test_module.py ,代码如下:

# 模块级别def setup_module(): print("setup module")def teardown_module(): print("teardown module")class TestDemo: # 执行类 前后分别执行setup_class teardown_class def setup_class(self): print("TestDemo setup_class") def teardown_class(self): print("TestDemo teardown_class") # 每个类里面的方法前后分别执行 setup, teardown def setup(self): print("TestDemo setup") def teardown(self): print("TestDemo teardown") def test_demo1(self): print("test demo1") def test_demo2(self): print("test demo2")class TestDemo1: def test_demo3(self): print("test demo3")
复制代码

运行结果:

setup moduleTestDemo setup_classTestDemo setuptest demo1TestDemo teardownTestDemo setuptest demo2TestDemo teardownTestDemo teardown_classtest demo3teardown module
复制代码

setup_module 和 teardown_module 在整个模块只执行一次,setup_class 和 teardown_class 在类里面只 执行一次,setup_method/setup 和 teardown_method/teardown 在每个方法前后都会调用。一般用的最多 的是方法级别(setup/teardown)和类级别(setup_class/teardown_class)。

pytest fixtures

pytest 中可以使用 @pytest.fixture 装饰器来装饰一个方法,被装饰的方法名可以作为一个参数传入 到测试方法中。可以使用这种方式来完成测试之前的初始化,也可以返回数据给测试函数。

将 FIXTURE 作为函数参数

通常使用 setup 和 teardown 来进行资源的初始化。如果有这样一个场景,测试用例 1 和 测试用例 3 需要依赖登录功能,测试用例 2 不需要依赖登录功能。这种场景 setup,teardown 无法实现,可以使用 pytest fixture 功能,在方法前面加个 @pytest.fixture 装饰器,加了这个装饰器的方法可以以参数的 形式传入到方法里面执行。例如在登录的方法,加上 @pytest.fixture 这个装饰器后,将这个用例 方法名以参数的形式传到方法里,这个方法就会先执行这个登录方法,再去执行自身的用例步骤,如果 没有传入这个登录方法,就不执行登录操作,直接执行已有的步骤。

创建一个文件名为 “test_fixture.py”,代码如下;

#!/usr/bin/env python# -*- coding: utf-8 -*-import pytest@pytest.fixture()def login(): print("这是个登录方法") return ('tom','123')@pytest.fixture()def operate(): print("登录后的操作")def test_case1(login,operate): print(login) print("test_case1,需要登录")def test_case2(): print("test_case2,不需要登录 ")def test_case3(login): print(login) print("test_case3,需要登录")
复制代码

在上面的代码中,测试用例 test_case1 和 test_case3 分别增加了 login 方法名作为参数,pytest 会发现 并调用 @pytest.fixture 标记的 login 功能,运行测试结果如下:

test_fixture.py::test_case1 这是个登录方法登录后的操作PASSED [ 33%]('tom', '123')test_case1,需要登录test_fixture.py::test_case2 PASSED \[ 66%]test_case2,不需要登录test_fixture.py::test_case3 这是个登录方法PASSED [100%]('tom', '123')test_case3,需要登录============================== 3 passed in 0.02s ===============================Process finished with exit code 0
复制代码

从上面的结果可以看出,test_case1 和 test_case3 运行之前执行了 login 方法,test_case2 没有执行这个 方法。

指定范围内共享

fixture 里面有一个参数 scope,通过 scope 可以控制 fixture 的作用范围,根据作用范围大小划分: session> module> class> function,具体作用范围如下:

 - function 函数或者方法级别都会被调用 - class 类级别调用一次 - module 模块级别调用一次 - session 多个文件/包调用一次(可以跨.py文件调用,每个.py文件就是module)
复制代码

例如整个模块有很多条测试用例,需要在全部用例执行之前打开浏览器,全部执行完之后去关闭浏览 器,打开和关闭操作只执行一次,如果每次都重新执行打开操作,会非常占用系统资源。这种场景除了 setup_module/teardown_module 可以实现,还可以通过设置模块级别的 fixture 装饰器 (@pytest.fixture(scope="module"))来实现。

scope='module'

fixture 参数 scope='module' ,module 作用是整个模块都会生效。

创建文件名为 test_fixture_scope.py ,代码如下:

#!/usr/bin/env python# -*- coding: utf-8 -*-import pytest# 作用域:module是在模块之前执行, 模块之后执行@pytest.fixture(scope="module")def open(): print("打开浏览器") yield print("执行teardown !") print("最后关闭浏览器")@pytest.mark.usefixtures("open")def test_search1(): print("test_search1") raise NameError passdef test_search2(open): print("test_search2") passdef test_search3(open): print("test_search3") pass
复制代码

代码解析:

@pytest.fixture() 如果不写参数,参数默认 scope='function' 。当 scope='module' 时,在当前 .py 脚本里面所有的用例开始前只执行一次。scope 巧妙与 yield 关键字结合使用,相当于 setup 和 teardown 方法。还可以使用 @pytest.mark.usefixtures 装饰器,传入前置函数名作为参数。

运行结果如下:

test_fixture_yield.py::test_search1 打开浏览器FAILED [ 33%]test_search1test_fixture_yield.py:13 (test_search1)open = None def test_search1(open): print("test_search1")> raise NameErrorE NameErrortest_fixture_yield.py:16: NameErrortest_fixture_yield.py::test_search2 PASSED \[ 66%]test_search2test_fixture_yield.py::test_search3 PASSED \[100%]test_search3执行teardown !最后关闭浏览器...open = None def test_search1(open): print("test_search1")> raise NameErrorE NameErrortest_fixture_yield.py:16: NameError...
复制代码

从上面运行结果可以看出, scope="module" 与 yield 结合,相当于 setup_module 和 teardown_module 方法。整个模块运行之前调用了 open() 方法中 yield 前面的打印输出“打开浏览 器”,整个运行之后调用了 yield 后面的打印语句“执行 teardown !”与“关闭浏览器”。

通过 yield 来唤醒 teardown 的执行,如果用例出现异常,不影响 yield 后面的 teardown 执行。

CONFTEST.PY 文件

fixture scope 为 session 级别是可以跨 .py 模块调用的,也就是当我们有多个 .py 文件的用例时, 如果多个用例只需调用一次 fixture,可以将 scope='session' ,并且写到 conftest.py 文件里。 写到 conftest.py 文件可以全局调用这里面的方法。使用的时候不需要导入 conftest.py 这个文 件。使用 conftest.py 的规则:

1. conftest.py 这个文件名是固定的,不可以更改。

2. conftest.py 与运行用例在同一个包下,并且该包中有 __init__.py 文件

3.使用的时候不需要导入 conftest.py ,pytest 会自动识别到这个文件

4.放到项目的根目录下可以全局调用,放到某个 package 下,就在这个 package 内有效。

案例

运行整个项目下的所有的用例,只执行一次打开浏览器。执行完所有的用例之后再执行关闭浏览器,可 以在这个项目下创建一个 conftest.py 文件,将打开浏览器操作的方法放在这个文件下,并添加一个 装饰器 @pytest.fixture(scope="session") ,就能够实现整个项目所有测试用例的浏览器复用,案 例目录结构如下:

创建目录 test_scope,并在目录下创建三个文件 conftest.py , test_scope1.py 和 test_scope2.py 。 conftest.py 文件定义了公共方法,pytest 会自动读取 conftest.py 定义的方法,代码如下:

#!/usr/bin/env python# -*- coding: utf-8 -*-import pytest@pytest.fixture(scope="session")def open(): print("打开浏览器") yield print("执行teardown !") print("最后关闭浏览器")
复制代码

创建“test_scopel.py”文件,代码如下:

#!/usr/bin/env python# -*- coding: utf-8 -*-import pytestdef test_search1(open): print("test_search1") passdef test_search2(open): print("test_search2") passdef test_search3(open): print("test_search3") passif __name__ == '__main__': pytest.main()
复制代码

创建文件“test_scope2.py”,代码如下:

#!/usr/bin/env python# -*- coding: utf-8 -*-class TestFunc(): def test_case1(self): print("test_case1,需要登录")def test_case2(self): print("test_case2,不需要登录 ") def test_case3(self): print("test_case3,需要登录")
复制代码

打开 cmd,进入目录 test_scope/,执行如下命令:

pytest -v -s
复制代码

或者

pytest -v -s test_scope1.py test_scope2.py
复制代码

执行结果如下:

省略...collected 6 itemstest_scope1.py::test_search1 打开浏览器test_search1PASSEDtest_scope1.py::test_search2 test_search2PASSEDtest_scope1.py::test_search3 test_search3PASSEDtest_scope2.py::TestFunc::test_case1 test_case1,需要登录PASSEDtest_scope2.py::TestFunc::test_case2 test_case2,不需要登录PASSEDtest_scope2.py::TestFunc::test_case3 test_case3,需要登录PASSED执行teardown !最后关闭浏览器省略后面打印结果...
复制代码

自动执行 FIXTURE

如果每条测试用例都需要添加 fixture 功能,则需要在每一条要用例方法里面传入这个 fixture 的名字, 这里就可以在装饰器里面添加一个参数 autouse='true' ,它会自动应用到所有的测试方法中,只是这 里没有返回值。 使用方法,在方法前面加上装饰器,如下:

@pytest.fixture(autouse="true")def myfixture(): print("this is my fixture")
复制代码

@pytest.fixture 里设置 autouse 参数值为 true(默认 false),每个测试函数都会自动调用这个前置 函数。 创建文件名为“test_autouse.py”,代码如下:

# coding=utf-8import pytest@pytest.fixture(autouse="true")def myfixture(): print("this is my fixture")class TestAutoUse: def test_one(self): print("执行test_one") assert 1 + 2 == 3 def test_two(self): print("执行test_two") assert 1 == 1 def test_three(self): print("执行test_three") assert 1 + 1 == 2
复制代码

执行上面这个测试文件,结果如下:

...test_a.py::TestAutoUse::test_one this is my fixture执行test_onePASSEDtest_a.py::TestAutoUse::test_two this is my fixture执行test_twoPASSEDtest_a.py::TestAutoUse::test_three this is my fixture执行test_threePASSED...
复制代码

从上面的运行结果可以看出,在方法 myfixture() 上面添加了装饰器 @pytest.fixture(autouse="true") ,测试用例无须传入这个 fixture 的名字,它会自动在每条用例 前执行这个 fixture。

FIXTURE 传递参数

测试过程中需要大量的测试数据,如果每条测试数据都编写一条测试用例,用例数量将是非常庞大的。 一般我们在测试过程中会将测试用到的数据以参数的形式传入到测试用例中,并为每条测试数据生成一 个测试结果。这时候可以使用 fixture 的参数化功能,在 fixture 方法加上装饰器 @pytest.fixture(params=[1,2,3]) ,就会传入三个数据 1、2、3,分别将这三个数据传入到用例当 中。这里传入的数据类型是个列表。传入的数据需要使用一个固定的参数名 request 来接收。

创建文件名为“test_params.py”,代码如下:

import pytest@pytest.fixture(params=[1, 2, 3])def data(request): return request.paramdef test_not_2(data): print(f"测试数据:{data}") assert data < 5
复制代码

运行结果如下:

...test_params.py::test_not_2[1]PASSED [ 33%]测试数据:1test_params.py::test_not_2[2] PASSED [ 66%]测试数据:2test_params.py::test_not_2[3] PASSED [100%]测试数据:3...
复制代码

从运行结果可以看出,对于 params 里面的每个值,fixture 都会去调用执行一次,使用 request.param 来 接受参数化的数据,并且为每一个测试数据生成一个测试结果。在测试工作中使用这种参数化的方式, 代码量会大大的减少,并且便于阅读与维护。

第三方插件介绍

控制用例的执行顺序

当一个目录下有很多模块,甚至有很多子目录。每个目录下也有大量的测试用例。pytest 加载所有的测 试用例的顺序是按照 pytest 的自定义的规则(不同的模块间是按照模块名字的 ASCII 码顺序排列,同 一模块间是按照定义的前后顺序加载的)。如果想指定用例的顺序,可以使用 pytest-ordering 插件,指 定用例的执行顺序只需要在测试用例的方法前面加上装饰器 @pytest.mark.run(order=[num]) 设置 order 的对应的 num 值,它就可以按照 num 的大小顺序来执行(由小到大的顺序执行,负数反之)。

应用场景:

有时运行测试用例需要指定它的顺序,比如有些场景需要先要登录,才能执行后续的流程(比如购物流 程,下单流程),这时就需要指定测试用例的顺序。通过 pytest-ordering 这个插件可以完成用例 顺序的指定。

安装

pip install pytest-ordering
复制代码

案例

创建一个测试文件“test_order.py”,代码如下

import pytestclass TestPytest(object): @pytest.mark.run(order=-1) def test_two(self): print("test_two,测试用例") @pytest.mark.run(order=3) def test_one(self): print("test_one,测试用例") @pytest.mark.run(order=1) def test_three(self): print("\ntest_three,测试用例")
复制代码

执行结果,如下查看执行顺序:

省略...test_order.py::TestPytest::test_threetest_order.py::TestPytest::test_onetest_order.py::TestPytest::test_two省略...
复制代码

从上面的执行结果可以看出,执行时以 order 的顺序执行:order=1,order=3,order=-1。(1 最先执 行,-1 代表倒数第一个执行)

多线程并行与分布式执行

应用场景:

假如项目中有测试用例 1000 条,一条测试用例需要执行 1 分钟,一个测试人员需要 1000 分钟才能 完成一轮回归测试。通常我们会用人力成本换取时间成本,加几个人一起执行,时间就会缩短。如果 10 人一起执行只需要 100 分钟,这就是一种并行测试,分布式的场景。

pytest-xdist 是 pytest 分布式执行插件,这款插件允许用户将测试并发执行(进程级并发),使用 这款插件执行用例是随机的,为了保证各个测试用例能在各自独立进程里正确的执行,应该保证测试用 例的独立性(这也符合测试用例设计的最佳实践)。

安装

pip install pytest-xdist
复制代码

多个 CPU 并行执行用例,需要在 pytest 后面添加 -n 参数,如果参数为 auto,会自动检测系统的 CPU 数目。如果参数为数字,则指定运行测试的处理器进程数。

pytest -n autopytest -n [num]
复制代码

案例

某个项目有 200 条测试用例,每条测试用例之间没有关联关系,互不影响。这 200 条测试用例需要在 1 小时之内测试完成,可以加个 -n 参数,使用多 CPU 并行测试。

运行方法

pytest -n 4
复制代码

进入到项目目录下,执行 pytest 可以将项目目录下所有测试用例识别出来并且运行,加上 -n 参数,可 以指定 4 个 CPU 并发执行。如果设置的 CPU 数大于系统 CPU 个数,则会按照当前 CPU 的实际个数 执行.

结合 PYTEST-HTML 生成测试报告

测试报告通常在项目中尤为重要,报告可以体现测试人员的工作量,开发人员可以从测试报告中了解缺 陷的情况,因此测试报告在测试过程中的地位至关重要,测试报告为纠正软件存在的质量问题提供依 据,为软件验收和交付打下基础。

测试报告根据内容的侧重点,可以分为 “版本测试报告” 和 “总结测试报告”。执行完 pytest 测试 用例,可以使用 pytest-html 插件生成 HTML 格式的测试报告。

安装

pip install pytest-html
复制代码

执行方法

pytest --html=path/to/html/report.html
复制代码

结合 pytest-xdist 使用

pytest -v -s -n 3 --html=report.html --self-contained-html
复制代码

生成测试报告

如下图:

生成的测试报告最终是 HTML 格式,报告内容包括标题、运行时间、环境、汇总结果以及用例的通过 个数、跳过个数、失败个数、错误个数,重新运行个数、以及错误的详细展示信息。 报告会生成在运行脚本的同一路径下,需要指定路径添加 --html=path/to/html/report.html 这个 参数配置报告的路径。如果不添加 --self-contained-html 这个参数,生成报告的 CSS 文件是独立 的,分享的时候容易数据丢失。

assert 断言使用

编写代码时,我们经常会做出一些假设,断言就是用于在代码中验证这些假设。断言表示为一些布尔表 达式,测试人员通常会加一些断言来判断中间过程的正确性。

断言支持最常见的表达式,包括调用,属性,比较以及二元和一元运算符。 Python 使用 assert(断 言)用于判断一个表达式的正确与否,在表达式条件为 False 的时候触发异常。

使用方法

assert True #断言为真assert not True #断言不为真
复制代码

案例如下:

assert "h" in "hello" #判断 h 在 hello 中assert 5>6 #判断 5>6 为真assert not True #判断不为真assert {'0', '1', '3', '8'} == {'0', '3', '5', '8'} #判断两个集合相等
复制代码

如果没有断言,没有办法判定用例中每一个测试步骤结果的正确性。在项目中适当的使用断言对代码的 结构、属性、功能、安全性等场景检查与验证。

搜索微信公众号:TestingStudio 霍格沃兹的干货都很硬核

用户头像

社区:ceshiren.com 微信:ceshiren2023 2022-08-29 加入

微信公众号:霍格沃兹测试开发 提供性能测试、自动化测试、测试开发等资料、实事更新一线互联网大厂测试岗位内推需求,共享测试行业动态及资讯,更可零距离接触众多业内大佬

评论

发布
暂无评论
软件测试 | pytest测试框架_测试_测吧(北京)科技有限公司_InfoQ写作社区