赞
踩
今日之失,未必不为后日之得。大家好,刚才在翻看之前整理的一些关于自动化测试的文档,突然发现一个比较详细的关于pytest框架的介绍和使用文章,对于打算使用python进行自动化测试编写的小伙伴,还是很值得一看的,希望对大家有所帮助。
一、前言
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(并发)
运行一个简单的用例:
- 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 --help
case文件为:
- import pytest
-
-
- def 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.smoke1
-
- def test_c():
-
- assert (1, 2, 3) == (1, 2, 3)
-
-
- @pytest.mark.smoke2
-
- def 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个失败然后重试会重试第四个,如果成功就继续往下执行。
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
Fixture的调用方式:
@pytest.fixture(scope = "function",params=None,autouse=False,ids=None,name=None)
参数详解:
1、SCOPE
用于控制Fixture的作用范围
作用类似于Pytest的setup/teardown
默认取值为function(函数级别),控制范围的排序为:session > module > class > 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 fixture
-
- def test_001(self, login):
-
- print("001传入了loging fixture")
-
- assert login == "account"
-
-
- #传入logout fixture
-
- def 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,这里就类似函数相互调用一样的逻辑
当测试类内的每一个测试方法都调用了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(固定写法,如下图)
举个栗子:
配置了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参数后,仍传入函数名称,会失败")
运行结果:
- import pytest
-
-
- @pytest.fixture()
-
- def login():
-
- print("今天的笔记做完了吗?")
-
- yield
-
- print("今天的笔记做完啦!!!")
-
-
-
- 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 中的返回数据也可以在测试函数中使用
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("输入账号密码")
-
- yield
-
- print("清理数据完成")
case文件:
- import pytest
-
-
- class 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文件只会调用一次
我们先来看下目录结构
内层case文件:b.py
- import pytest
-
-
- class TestLogin1():
-
- # 调用了内层fixture
-
- def test_1(self, login2):
-
- print("用例1")
-
-
- # 调用了外层fixture
-
- def 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 pytest
-
-
- class TestLogin1():
-
- # 调用了内层fixture
-
- def test_1(self, login2):
-
- print("用例1")
-
- # 调用了外层fixture
-
- def 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.mark.skip(self,reason=None)
-
- def test_one():
-
- pass
参数说明:
用法:
- @pytest.mark.skipif(self,condition, reason=None)
-
- def test_two():
-
- pass
参数说明:
两者的区别
@pytest.mark.skip:无差别跳过,只要用这个mark就跳过
@pytest.mark.skipif:符合条件的才跳过
举个栗子:
- import pytest
-
-
- # 设置跳过condition
-
- con = "跳过条件"
-
- @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==2
-
-
-
- if __name__ == '__main__':
-
- pytest.main()
运行结果
前言
@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
运行结果:
总结:
Pytest允许用户自定义自己的用例标签,用于将用例进行分组,以便在运行用例的时候筛选你想要运行的用例。
- import pytest
-
-
-
- @pytest.mark.login
-
- class TestLogin:
-
- """
- 登陆功能测试类
- """
-
-
- @pytest.mark.smoke
-
- @pytest.mark.success
-
- def test_login_sucess(self):
-
- """
- 登陆成功
- """
-
- # 实现登陆逻辑
-
- pass
-
-
-
- @pytest.mark.failed
-
- def test_login_failed(self):
-
- """
- 登陆失败
- """
-
- # 实现登陆逻辑
-
- pass
-
-
-
-
-
- @pytest.mark.logout
-
- class TestLogout:
-
- """
- 登出功能测试类
- """
-
-
- @pytest.mark.smoke
-
- @pytest.mark.success
-
- def test_logout_sucess(self):
-
- """
- 登出成功
- """
-
- # 实现登出功能
-
- pass
-
-
-
- @pytest.mark.failed
-
- def test_logout_failed(self):
-
- """
- 登出失败
- """
-
- # 实现登出功能
-
- pass
运行方法:
语法:
pytest -m "自定义标签名"
或:
pytest.main(['-m 自定义标签名'])
示例:
pytest -m "smoke" testmark.py
运行结果:
从以上运行结果可以看出,只有被选择的标记用例被运行
- # 运行登陆功能的用例
-
- 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'])
当使用 -m 参数执行 mark 标记的用例时,pytest 会发出告警信息 “PytestUnknownMarkWarning: Unknown pytest.mark.login - is this a typo? ”,告诉你这是一个 pytest 未知的一个标记!为了消除告警,我们需要在 pytest 的配置文件中注册 mark 标记!
注册 mark 标记:
- [pytest]
-
- markers =
-
- login : 'marks tests as login'
-
- logout : 'marks tests as logout'
-
- success : 'marks tests as success'
-
- failed : 'marks tests as failed'
添加之后再看,警告信息就不再有了:
注册完 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 配置文件不支持注释,不支持注释,不支持注释…
一、需要安装如下环境:
- 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 pytest
-
- import time
-
-
- def 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 pytest
-
- import time
-
-
- def 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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。