今日之失,未必不为后日之得。大家好,刚才在翻看之前整理的一些关于自动化测试的文档,突然发现一个比较详细的关于pytest框架的介绍和使用文章,对于打算使用python进行自动化测试编写的小伙伴,还是很值得一看的,希望对大家有所帮助。
Pytest简介
一、前言
pytest是python一个第三方单元测试框架,有非富的第三方插件可以扩展,例如:Allure,兼容unittest但比原生未做功能增强的unittest其更高效简洁。
二、pytest安装
1.pytest通过pip安装即可,安装命令:
pip install pytest
2.查看是否安装成功
pytest --version
三、Pytest的常用功能
1.用例运行规则
2.用例运行的级别
3.@pytest.fixture使用
4.@pytest.mark使用
5.插件之pytest-ordering(用例执行顺序)
6.插件之pytest-rerunfailures(失败重跑)
7.插件之pytest-assume(多断言)
8.插件之pytest-xdist(并发)
Pytest运行及控制台输出信息
运行一个简单的用例:
def test_passing():assert (1, 2, 3) == (1, 2, 3)
运行结果及说明:
测试运行可能出现的结果总结(上图6、7运行结果列举)
示例:
import pytest# 测试通过def test_passing():assert (1, 2, 3) == (1, 2, 3)# 测试失败def test_failing():assert (1, 2, 3) == (3, 2, 1)# 跳过不执行@pytest.mark.skip()def test_skip():assert (1, 2, 3) == (3, 2, 1)# 预期失败,确实失败@pytest.mark.xfail()def test_xfail():assert (1, 2, 3) == (3, 2, 1)# 预期失败,但是结果pass@pytest.mark.xfail()def test_xpass():assert (1, 2, 3) == (1, 2, 3)
运行结果:
Pytest常用命令行选项--case相关
pytest 命令运行语法
pytest --help
常用case相关参数说明
详细举例说明
case文件为:
import pytestdef test_a():assert (1, 2, 3) == (1, 2, 3)def test_failing():assert (1, 2, 3) == (3, 2, 1)def test_failing2():assert (1, 2, 3) == (3, 2, 1)def test_b():assert (1, 2, 3) == (1, 2, 3)@pytest.mark.smoke1def test_c():assert (1, 2, 3) == (1, 2, 3)@pytest.mark.smoke2def test_d():assert (1, 2, 3) == (1, 2, 3)
1、–collect–only:收集目录下所有的用例
pytest -–collect-only
2、-k:模糊筛选指定的case,只要匹配到了就,就收集运行
pytest -k "a or b"
从运行结果可以看出,-k是会按条件模糊查找case名称中的信息,只要匹配到了就,就收集运行
3、-m:运行带用指定标记的用例,常于@pytest.mark结合使用
pytest -m “smoke1 or smork2” test_two.py
注:上图的mark警告信息是由于,mark标记 smoke1等不是fixture自带的标记,为自定义标记,所以报错,解决办法将在@pytest.mark内详细说明
4、-x:遇到失败停止运行脚本
上图可以看出,遇到停卡,其它几条用例就不再运行
5、–maxfail=num:达到num错误次数再停卡整个case,此参数与-x类似,都是遇错误终止
pytest --maxfail=2 test_two.py
6、--lf(--last-failed):运行最后一次失败用例,且只运行失败的case(前提是已经运行过的用例有失败的一次运行,-x我们已经有过一次失败了)
pytest --lf test_two.py
7、--ff(--failed-first):同lf相似,但–ff是所有的case都会运行,只是优先运行上次失败的case,再运行其它状态的case
pytest --lf test_two.py
失败重试:
• 测试失败后要重新运行n次,要在重新运行之间添加延迟时间,间隔n秒再运行。
• 安装:pip install pytest-rerunfailures
• pytest -v - -reruns 5 --reruns-delay 1 — 每次等1秒 重试5次
比如:10个用例执行到第4个失败然后重试会重试第四个,如果成功就继续往下执行。
Pytest前后置处理函数Fixture简介
Fixture是pytest的核心功能,也是亮点功能.属装饰器函数(在不改变被装饰函数的前提下对函数进行功能增强),用于在测试用例运行之前进行前后置工作处理工作。与setup/teardown类似,但更强大灵活
Fixture简介
Fixture的目的是提供一个固定基线,在该基线上测试可以可靠地和重复地执行。fixture提供了区别于传统单元测试(setup/teardown)有显著改进:
1.有独立的命名,并通过声明它们从测试函数、模块、类或整个项目中的使用来激活。
2.按模块化的方式实现,每个fixture都可以互相调用。
Fixture的范围从简单的单元扩展到复杂的功能测试,允许根据配置和组件选项对fixture和测试用例进行参数化,或者跨函数function、类class、模块module或整个测试会话sessio范围。
Fixture工作原理
@Pytest.fixture()装饰器用于声明函数是一个fixture
如果测试函数的参数列表中包含fixture装饰的函数名
那么pytest就会检测到然后在测试函数运行之前执行该fixture
如果fixture装饰的函数有返回值,那么fixture在完成任务后,将数据再返回给测试函数,相当于传参。
Pytest搜索fixture的顺序
测试用例的参数列表中包含一个fixture名,pytest会用该名称搜索fixture
优先搜索测试所在的模块
然后再搜索模块同一文件路径下的conftest.py
找不到再搜索上一层的conftest
Pytest之Fixture参数详解及使用
Fixture的调用方式:
@pytest.fixture(scope = "function",params=None,autouse=False,ids=None,name=None)
参数详解:
1、SCOPE
用于控制Fixture的作用范围
作用类似于Pytest的setup/teardown
默认取值为function(函数级别),控制范围的排序为:session > module > class > function
作用范围举例:
scope = “function”
语法:
@pytest.fixture() # 或者@pytest.fixture(scope='function')
场景一:做为参数传入
import pytest# fixture函数(类中) 作为多个参数传入@pytest.fixture()def login():print("打开浏览器")a = "account"return a@pytest.fixture()def logout():print("关闭浏览器")class TestLogin:#传入lonin fixturedef test_001(self, login):print("001传入了loging fixture")assert login == "account"#传入logout fixturedef test_002(self, logout):print("002传入了logout fixture")def test_003(self, login, logout):print("003传入了两个fixture")def test_004(self):print("004未传入仍何fixture哦")if __name__ == '__main__':pytest.main()
运行结果:
从运行结果可以看出,fixture做为参数传入时,会在执行函数之前执行该fixture函数。再将值传入测试函数做为参数使用,这个场景多用于登录
场景二、Fixture的相互调用
代码:
import pytest# fixtrue作为参数,互相调用传入@pytest.fixture()def account():a = "account"print("第一层fixture")return a# fixture的相互调用一定是要在测试类里调用这层fixture才会生次,普通函数单独调用是不生效的@pytest.fixture() def login(account):print("第二层fixture")class TestLogin:def test_1(self, login):print("直接使用第二层fixture,返回值为{}".format(login))def test_2(self, account):print("只调用account fixture,返回值为{}".format(account))if __name__ == '__main__':pytest.main()
运行结果:
注:
1.即使fixture之间支持相互调用,但普通函数直接使用fixture是不支持的,一定是在测试函数内调用才会逐级调用生效
2.有多层fixture调用时,最先执行的是最后一层fixture,而不是先执行传入测试函数的fixture
3.上层fixture的值不会自动return,这里就类似函数相互调用一样的逻辑
scope = “class”:
当测试类内的每一个测试方法都调用了fixture,fixture只在该class下所有测试用例执行前执行一次
测试类下面只有一些测试方法使用了fixture函数名,这样的话,fixture只在该class下第一个使用fixture函数的测试用例位置开始算,后面所有的测试用例执行前只执行一次。而该位置之前的测试用例就不管。
语法:
@pytest.fixture(scope='class')
场景一、
import pytest# fixture作用域 scope = 'class'@pytest.fixture(scope='class')def login():print("scope为class")class TestLogin:def test_1(self, login):print("用例1")def test_2(self, login):print("用例2")def test_3(self, login):print("用例3")if __name__ == '__main__':pytest.main()
运行结果:
场景二、
import pytest@pytest.fixture(scope='class')def login():a = '123'print("输入账号密码登陆")class TestLogin:def test_1(self):print("用例1")def test_2(self, login):print("用例2")def test_3(self, login):print("用例3")def test_4(self):print("用例4")if __name__ == '__main__':pytest.main()
运行结果:
scope = “module”:与class相同,只从.py文件开始引用fixture的位置生效
import pytest# fixture scope = 'module'@pytest.fixture(scope='module')def login():print("fixture范围为module")def test_01():print("用例01")def test_02(login):print("用例02")class TestLogin():def test_1(self):print("用例1")def test_2(self):print("用例2")def test_3(self):print("用例3")if __name__ == '__main__':pytest.main()
运行结果:
scope = “session”:用法将在conftest.py文章内详细介绍
session的作用范围是针对.py级别的,module是对当前.py生效,seesion是对多个.py文件生效
session只作用于一个.py文件时,作用相当于module
所以session多数与contest.py文件一起使用,做为全局Fixture
2、params:
Fixture的可选形参列表,支持列表传入
默认None,每个param的值
fixture都会去调用执行一次,类似for循环
可与参数ids一起使用,作为每个参数的标识,详见ids
被Fixture装饰的函数要调用是采用:Request.param(固定写法,如下图)
举个栗子:
3、ids:
- 用例标识ID
- 与params配合使用,一对一关系
举个栗子:
未配置ids之前,用例:
配置了IDS后:
4、autouse:
默认False
若为True,刚每个测试函数都会自动调用该fixture,无需传入fixture函数名
由此我们可以总结出调用fixture的三种方式:
1.函数或类里面方法直接传fixture的函数参数名称
2.使用装饰器@pytest.mark.usefixtures()修饰
3.autouse=True自动调用,无需传仍何参数,作用范围跟着scope走(谨慎使用)
让我们来看一下,当autouse=ture的效果:
5、Name:
fixture的重命名
通常来说使用 fixture 的测试函数会将 fixture 的函数名作为参数传递,但是 pytest 也允许将fixture重命名
如果使用了name,那只能将name传如,函数名不再生效
调用方法:
@pytest.mark.usefixtures(‘fixture1’,‘fixture2’)
举栗:
import pytest@pytest.fixture(name="new_fixture")def test_name():pass# 使用name参数后,传入重命名函数,执行成功def test_1(new_fixture):print("使用name参数后,传入重命名函数,执行成功")# 使用name参数后,仍传入函数名称,会失败def test_2(test_name):print("使用name参数后,仍传入函数名称,会失败")
运行结果:
Pytest前后置处理函数Fixture之yiled
import pytest@pytest.fixture()def login():print("今天的笔记做完了吗?")yieldprint("今天的笔记做完啦!!!")def test_01(login):print("我是用例一")if __name__ == '__main__':pytest.main()
运行结果:
从以上运行结果我们可以看到,带有yiled的fixture函数,yiled函数语句分别在测试类前后被执行,我们来总结一下带yiled的fixture特性。
总结:
一般的fixture 函数会在测试函数之前运行,但是如果 fixture 函数包含 yiled,那么会在 yiled处停止并转而运行测试函数,等测试函数执行完毕后再回到该 fixture 继续执行 yiled 后面的代码。
可以将 yiled前面的代码看作是 setup,yiled 后面的部分看作是 teardown 的过程。
无论是测试函数中发生了什么是成功还是失败或者 error 等情况,yiled 后面的代码都会被执行,yiled 中的返回数据也可以在测试函数中使用
Pytest之conftest.py
conftest.py是什么?
conftest.py是fixture函数的一个集合,可以理解为公共的提取出来放在一个文件里,然后供其它模块调用。不同于普通被调用的模块,conftest.py使用时不需要导入,Pytest会自动查找
conftest.py使用场景
如果我们有很多个前置函数,写在各个py文件中是不很乱?再或者说,我们很多个py文件想要使用同一个前置函数该怎么办?这也就是conftest.py的作用
conftest.py使用原则
conftest.py这个文件名是固定的,不可以更改。
conftest.py与运行用例在同一个包下,并且该包中有__init__.py文件使用的时候
不需要导入conftest.py,会自动寻找。
conftest.py使用举例
conftest.py文件(scope=“session”)
import pytest@pytest.fixture(scope="session")def login():print("输入账号密码")yieldprint("清理数据完成")
case文件:
import pytestclass TestLogin1():def test_1(self, login):print("用例1")def test_2(self):print("用例2")def test_3(self, login):print("用例3")if __name__ == '__main__':pytest.main()
运行结果
从上图可以看出,conftest.py内的fixture方法的作用范围是session,调用时,整个.py文件只会调用一次
conftest.py作用域
我们先来看下目录结构
内层case文件:b.py
import pytestclass TestLogin1():# 调用了内层fixturedef test_1(self, login2):print("用例1")# 调用了外层fixturedef test_3(self, login):print("用例3")if __name__ == '__main__':pytest.main()
内层conftest.py
import pytest@pytest.fixture(scope="session")def login2():print("内层fixture前置")
外层case:a.py
import pytestclass TestLogin1():# 调用了内层fixturedef test_1(self, login2):print("用例1")# 调用了外层fixturedef test_3(self, login):print("用例3")if __name__ == '__main__':pytest.main()
外层conftest.py
import pytest@pytest.fixture(scope="session")def login():print("外层fixture")
运行内层b.py
由上图过行结果可以看出,内外层的conftest.py都可以被找到并被调用执行
我们再来运行一下外层case,a.py
可以看到,外层调用内层的fixture是失败的
由以上运行结果可以总结出:
conftest.py的作用域与Python变量作用域相同
内层包内conftest.py不允许被其它包的测试类或方法使用,相当于本地变量
外层conftest.py可被内层测试类和方法使用,相当于全局变量
Pytest跳过用例之@pytest.mark.skip和@pytest.mark.skipif
@pytest.mark.skip:跳过该条测试用例
用法:
@pytest.mark.skip(self,reason=None)def test_one():pass
参数说明:
@pytest.mark.skipif:跳过符合条件的测试用例
用法:
@pytest.mark.skipif(self,condition, reason=None)def test_two():pass
参数说明:
两者的区别
@pytest.mark.skip:无差别跳过,只要用这个mark就跳过
@pytest.mark.skipif:符合条件的才跳过
举个栗子:
import pytest# 设置跳过conditioncon = "跳过条件"@pytest.mark.skip("这条是skip,无差别跳过")def test_mark_skip():assert 1==1@pytest.mark.skipif(con == "跳过条件",reason='skipif有条件的跳过')def test_mark_skipif():assert 1==1@pytest.mark.skipif(con == "不符合",reason='skipif不符合条件的执行')def test_mark_noskipif():assert 2==2if __name__ == '__main__':pytest.main()
运行结果
Pytest之调用Fixture@pytest.mark.usefixtures
前言
@pytest.mark.usefixtures是Pytest调用fixture的方法之一,与直接传入fixture不同的是,它无法获取到被fixture装饰的函数的返回值。
@pytest.mark.usefixtures的使用场景是被测试函数需要多个fixture做前后置工作时使用
用法
@pytest.mark.usefixtures(self,*fixturenames)def test_one():pass
参数说明:
举栗
场景一:传入单个Fixture
import pytest@pytest.fixture()def one():print("我是一个fixture函数")@pytest.mark.usefixtures('one')def test_one_fixture():print("测试用例一")assert 1==1
运行结果:
场景一:传入多个Fixture
import pytest@pytest.fixture()def one():print("我是第一个fixture函数")@pytest.fixture()def two():print("我是第二个fixture函数")@pytest.mark.usefixtures('one','two')def test_one_fixture():print("测试用例一")assert 1==1
运行结果
场景一:叠加多个Fixture
import pytest@pytest.fixture()def one():print("我是第一个fixture函数")@pytest.fixture()def two():print("我是第二个fixture函数")@pytest.mark.usefixtures('one')@pytest.mark.usefixtures('two')def test_one_fixture():print("测试用例一")assert 1==1
运行结果:
总结:
- 单个fixture:在被测试函数之前执行
- 多个fixture: 按传值先后顺序执行
- 叠加fixture: 自下至上执行,离测试函数越近的先被执行
Pytest之@pytest.mark.自定义标签使用
前言:
Pytest允许用户自定义自己的用例标签,用于将用例进行分组,以便在运行用例的时候筛选你想要运行的用例。
@pytest.mark.自定义标签的使用:
- 可以标记测试方法、测试类,标记名可以自定义,最好起有意义的名字;
- 同一测试类/方法可同时拥有多个标记;
import pytest@pytest.mark.loginclass TestLogin:"""登陆功能测试类"""@pytest.mark.smoke@pytest.mark.successdef test_login_sucess(self):"""登陆成功"""# 实现登陆逻辑pass@pytest.mark.faileddef test_login_failed(self):"""登陆失败"""# 实现登陆逻辑pass@pytest.mark.logoutclass TestLogout:"""登出功能测试类"""@pytest.mark.smoke@pytest.mark.successdef test_logout_sucess(self):"""登出成功"""# 实现登出功能pass@pytest.mark.faileddef test_logout_failed(self):"""登出失败"""# 实现登出功能pass
运行方法:
语法:
pytest -m "自定义标签名"
或:
pytest.main(['-m 自定义标签名'])
示例:
pytest -m "smoke" testmark.py
运行结果:
从以上运行结果可以看出,只有被选择的标记用例被运行
组合运行用例
- 使用 -m 参数运行标记的测试用例;
- -m 参数支持 and、or 、not 等表达式;
# 运行登陆功能的用例pytest.main(['-m login'])# 运行登出功能的用例pytest.main(['-m logout'])# 运行功能成功的用例pytest.main(['-m success'])# 运行功能失败的用例pytest.main(['-m failed'])# 运行登陆功能但是不运行登陆失败的测试用例pytest.main(['-m login and not failed'])# 运行登出功能但是不运行登出成功的测试用例pytest.main(['-m logout and not success'])# 运行登陆和登出的用例pytest.main(['-m login or logout'])
注册、管理 mark 标记:
当使用 -m 参数执行 mark 标记的用例时,pytest 会发出告警信息 “PytestUnknownMarkWarning: Unknown pytest.mark.login - is this a typo? ”,告诉你这是一个 pytest 未知的一个标记!为了消除告警,我们需要在 pytest 的配置文件中注册 mark 标记!
注册 mark 标记:
- 首先在项目根目录创建一个文件 pytest.ini ,这个是 pytest 的配置文件; 然后在 pytest.ini 文件的
- markers 中写入你的 mark 标记, 冒号 “:” 前面是标记名称,后面是 mark 标记的说明,可以是空字符串;
- 注意:pytest.ini 文件中只能使用纯英文字符,绝对不能使用中文的字符(尤其是冒号和空格)!
[pytest]markers =login : 'marks tests as login'logout : 'marks tests as logout'success : 'marks tests as success'failed : 'marks tests as failed'
添加之后再看,警告信息就不再有了:
规范使用 mark 标记:
注册完 mark 标记之后 pytest 便不会再告警,但是有时手残容易写错 mark 名,导致 pytest 找不到用例,一时想不开很难debug,尤其是团队协作时很容易出现类似问题,所以我们需要 “addopts = --strict” 参数来严格规范 mark 标记的使用!
在 pytest.ini 文件中添加参数 “addopts = --strict”;
[pytest]markers =smoke : 'marks tests as login'login : 'marks tests as login'logout : 'marks tests as logout'success : 'marks tests as success'failed : 'marks tests as failed'addopts = --strict
*注意要另起一行,不要在 markers 中添加;
添加该参数后,当使用未注册的 mark 标记时,pytest会直接报错:“ ‘xxx’ not found in markers configuration option ”,不执行测试任务;
注意:pytest.ini 配置文件不支持注释,不支持注释,不支持注释…
Pytest使用-多线程多进程运行
一、需要安装如下环境:
pip install pytest-xdist 或 pip install pytest-parallel
二、对比说明:
pytest-parallel比pytst-xdist相对好用,功能支持多。
pytest-xdist不支持多线程,
而pytest-parallel支持python3.6及以上版本,如果是想做多进程并发的需要在linux平台或mac上做,windows上不起作用即(workers永远=1),如果是做多线程的Linux/Mac/Windows平台都支持,进程数为workers设置的值。
三、pytest-xdist常用命令配置如下:
-n:进程数,也就是cpu个数 可以指定个数,最大值为当前机器cpu个数,也可以设置为auto,自动识别cpu个数。
示例:pytest -s test_multiProcess.py -n=2 #用2个进程运行py用例文件
细节使用参照官网:https://pypi.org/project/pytest-xdist/
import pytestimport timedef testcase_01():time.sleep(2)print('这里是testcase_01')def testcase_02():time.sleep(4)print('这里是testcase_02')def testcase_03():time.sleep(5)print('这里是testcase_03')if __name__ == '__main__':pytest.main(['-s',__file__,'-n=4'])
# 输出
......
gw0 I / gw1 I / gw2 I / gw3 I
gw0 [3] / gw1 [3] / gw2 [3] / gw3 [3]
...
============ 3 passed in 6.81s ========
# 总结:非多进程:9.07s 1cpu:10.59s 2cpu:7.72s 3cpu:4.98s 4cpu:5.32s
四、pytest-parallel常用配置命令如下:
–workers (optional) *:多进程运行需要加此参数, *是进程数。默认为1。
–tests-per-worker (optional) *:多线程运行, *是每个worker运行的最大并发线程数。默认为1
使用示例:
pytest test.py --workers 3:3个进程运行
pytest test.py --tests-per-worker 4:4个线程运行
pytest test.py --workers 2 --tests-per-worker 4:2个进程并行,且每个进程最多4个线程运行,即总共最多8个线程运行。
参考:https://pypi.org/project/pytest-parallel/
代码示例:
import pytestimport timedef testcase_01():time.sleep(2)print('这里是testcase_01')def testcase_02():time.sleep(4)print('这里是testcase_02')def testcase_03():time.sleep(5)print('这里是testcase_03')def testcase_04():time.sleep(9)print('这里是testcase_04')if __name__ == '__main__':pytest.main(['-s', __file__,'--workers=1','--tests-per-worker=4'])
总结:
单线程:20.08s(单个线程累计时长)
2个线程:13.06 3个线程:11.05 4个线程:9.06
【特别注意】:
1.pytest-parallel的workers参数在windows系统下永远是1,在linux和mac下可以取不同值。
2..pytest-parallel加了多线程处理后,最后执行时间是运行时间最长的线程的时间。
3.在windows下想用多进程的选pytst-xdist; 想用多线程的选pytest-paralle