pytest是一个非常好用的测试框架,并且这个框架已经非常成熟了。
它简单灵活,容易上手,支持参数化,也能够支持简单的单元测试和复杂的功能测试。也是自动化测试的利器。
我们先来看看如何安装:在Python环境没问题的情况下直接pip install pytest即可,进入正题
命令行参数
vs
先来看一个比较简单的示例,我们直接从示例入手:
import pytest def test_01(): print("第一条用例") class Testcase: def test_02(self): print("第二条测试用例")
注意看我的类名,首字母大写的!!!这里实际上是不会在控制台输出我们的打印信息的,所以我们还需要改改:
import pytest def test_01(): print("第一条用例") class Testcase: def test_02(self): print("第二条测试用例") if __name__ == '__main__': pytest.main(["-s","-v","running.py"])
-s是显示打印信息,-v显示详细的测试结果,这里你也可以写到一起去,也就是:
if __name__ == '__main__': pytest.main(["running.py","-vs"])
除了写main方法可以输出结果之外,我们开始控制台直接码命令。
可能你会遇到no tests ran in 0.01s这样的情况,这种情况一般都是目录下找不到执行文件,这时候你就需要cd 切换到文件所在的文件目录就好了。执行命令也是大同小异。
pytest running.py -vs
maxfail
当maxfail=1的时候,用例里面出现一个错误用例停止运行
接下来我们多写几个用例:
def test_01(): print("第一条用例") assert 0 > 1 def test_04(): print("清安无别事") class Testcase: def test_02(self): print("第二条测试用例") def test_03(self): print("第三条用例")
我是在控制台码命令的,所以这里我就不写main方法去跑了。
pytest running.py -vs --maxfail=1
xdist
pip install --xdist
多线程,在用例少的情况下看不出太大的区别,多线程要比单线程少节省将近一半的时间,也就是说当xdist 为2时,启用两个线程,比直接pytest跑节省近一半时间。
def test_01(): print("第一条用例") def test_04(): print("清安无别事") class Testcase: def test_02(self): print("第二条测试用例") def test_03(self): print("第三条用例")
pytest running.py -vs --maxfail=1 -n 2,这里xdist有缩写,也就是-n,后面的2就是2个线程的意思。注意看下方截图的红色框框的位置,就是线程的名字。
reruns
pip install pytest-rerunfailures
重试运行,当运行出现错误的时候,也就是非预期结果的时候我们可以再次运行一次该用例
reruns为2时,重新运行两次
def test_01(): print("第一条用例") assert 0>1 def test_04(): print("清安无别事") class Testcase: def test_02(self): print("第二条测试用例") def test_03(self): print("第三条用例")
pytest running.py -vs - n 2 --reruns 2,看下方的截图,重复运行的两次。
测试报告
pytest框架自带了一个测试报告生成器。--html=路径/名称.html
def test_01(): print("第一条用例") assert 0>1 def test_04(): print("清安无别事") class Testcase: def test_02(self): print("第二条测试用例") def test_03(self): print("第三条用例")
如果我想将这四个用例生成测试报告:
pytest running.py -vs - n 2 --reruns 2 --html=report/report.html
在report文件夹下生成一个report.html报告,这个报告虽然很一般 ,也是不错了。当然我们也可以生成allure测试报告,美观一些。
allure
pip install allure-pytest下载好库。
pytest running.py -vs --alluredir ../report/xmll
allure generate --clean ../report/xmll -o ./result/html
仔细看你就会发现其实--clean后面接的是运行代码时-o后面的路径,因为运行pytest running.py -vs --alluredir ../report/xmll后,文件下会自行创建一个report文件,用例运行信息会存在xmll里面,我们生成报告就需要调用里面的信息。
因为我是控制台输出的所以需要这两行代码,输出完后就可以在result文件下看到一个index.html文件,就是测试报告了。如下图所示:
mark标签
什么是mark标签?mark标签分为内置标签与外置标签,可以用来对用例进行管理运行,我们直接看看例子:
def test002(): print('这是一条最优级别的用例') def test005(): print("skip_reason") def test003(): print('这是一条严重级别的用例') def test006(): print("006冒烟") def test004(): print('这是一条较严重级别的用例')
上述例子的情况下,与正常情况下无异,这里需要注意:
pytest与unittest不一样,我test序列号写的很乱,如果是在unittest中的话肯定就是按数字的顺序来执行了,pytest中不分先后,05在04用例的前面,那么就得按这顺序来,先运行05用例再04用例。
那么我们给这里加上mark标签看看:
def test002(): print('这是一条最优级别的用例') def test005(): print("005冒烟") @pytest.mark.qingan def test003(): print('这是一条严重级别的用例') def test006(): print("006冒烟") @pytest.mark.qinganan def test004(): print('这是一条较严重级别的用例')
猜猜这里运行几条用例?3条,没错。
注意:这里mark标签是外置标签,我自己命名的。看看运行结果:
pytest -m qingan test_speack.py -sv这里指定外置标签的标签名。不指定就会运行全部的用例,不指定怎么对用例进行管理。
这里还是可以看到只执行了两条,也就是标签的那两条用例。这里对指定用例进行标记运行。
mark标签会受外界的装饰器影响吗?我们来看看。
def func(function): def fn(*args,**kwargs): print("this is "+function.__name__) return function(*args,**kwargs) return fn # ----------上面是自己写的装饰器-------------- @func def test002(): print('这是一条最优级别的用例') # @pytest.mark.skip def test005(): print("skip_reason") @pytest.mark.qingan @func def test003(): print('这是一条严重级别的用例') def test006(): print("006冒烟") @pytest.mark.qingan @func def test004(): print('这是一条较严重级别的用例')
是不会的,也是遵循pytest强大的执行顺序来的。
那么看了自定义标签,看看内置标签吧:
def func(function): def fn(*args,**kwargs): print("this is "+function.__name__) return function(*args,**kwargs) return fn # ----------上面是自己写的装饰器-------------- @func def test002(): print('这是一条最优级别的用例') @pytest.mark.skip def test005(): print("skip_reason") @pytest.mark.qingan @func def test003(): print('这是一条严重级别的用例') def test006(): print("006冒烟") @pytest.mark.qingan @func def test004(): print('这是一条较严重级别的用例')
我在05用例处添加了一个内置标签,这里需要注意的是,执行命令不一样:
pytest test_speack.py -s -rs或者pytest -k qingan test_speack.py -sv
05被跳过了,我们可以更改内置标签的提示信息
@pytest.mark.skip(reason='不运行') def test005(): print("skip_reason")
控制台就会有这样的一条信息可以看到。
这里可以看到自定义标签与内置标签处用的运行命令有所不同,不同之处在于一个用了-m一个用了-k。
使用-m的时候我们需要指定自定义标签,才能跑用例,否则跑不起来,如果用-k即使不指定也是可以跑起来,这样的一个好处,相信不说你也知道了,控制用例执行,可以更好的执行你想执行的用例。更好的是用于冒烟测试。
另外还有一个也就是内置标签判断,skipif。这是一个怎么样的标签呢?
看skip后面带的if就知道了,可以做一个判断!条件为真则跳过运行,条件为假则运行用例。
@func def test002(): print('这是一条最优级别的用例') @pytest.mark.skipif(1>2,reason='不运行') def test005(): print("skip_reason") @pytest.mark.qingan @func def test003(): print('这是一条严重级别的用例') def test006(): print("006冒烟") @pytest.mark.qingan @func def test004(): print('这是一条较严重级别的用例')
pytest -k test_speack.py -sv我在用例005处写了一个skipif的判断,1>2是FALSE,所以此处不跳过。
所以此处为真的条件就不举例了,此处直接告诉各位答案了。
前置后置
在unittest你肯定看到过setup这一类的函数,看下面的例子
class Testcasse(): """前置后置""" def setup(self): print("1每个用例的前置") def teardown(self): print("2每个用例的后置") def setup_class(self): print("3所有用例的前置") def teardown_class(self): print("4所有用例的后置") def test_004(self): print("004") def test_005(self): print("005")
其中的顺序是怎么样的呢。是否跟之前说的全部无优先级呢?运行一下就知道了。
这里就很明显了,优先级是3124,这里别误会,我在print里面加了数字,说一 这里就这样简写了。下面两个用例004,005是用于分辨的,不然运行不起来。各位可以自己跑一跑。这与pytest本身的规则有关系。
fixture
这里很容易弄错写成fixtrue,所以需要注意一些。怎么用呢?fixture可以实现我们在pytest的用例中调佣其他函数,可以发现我们使用fixture是在被调用函数之上添加我们的fixture。看例子:
@pytest.fixture def login(): print("login") return "运行用例" def test_seacher(): print(login) # 返回的是一个地址 print(f"搜索用户名{login}") def test_seacher1(login): print(login) print(f"搜索用户名{login}")
pytest text_fixtrue.py -sv运行起来看看效果:
这样看就不难理解了。这是一种简单的距离,看起来像是调用。
fixture区别于unnitest的传统单元测试(setup/teardown)有显著改进。
fixture里面有个scope参数可以控制fixture的作用范围:session>module>class>function
-function:每一个函数或方法都会调用
-class:每一个类调用一次,一个类中可以有多个方法
-module:每一个.py文件调用一次,该文件内又有多个function和class
-session:是多个文件调用一次,可以跨.py文件调用,每个.py文件就是module
这里就需要用到了conftest.py配置了。可以实现数据共享,比如py跨文件共享前置,另外需要注意的是conftest.py脚本名称是固定的,不更改名称。pytest会自动寻找这个文件。
如果此文件放到项目的根目录下就可以全局调用了,如果放到某个package下,那就在改package内有效。
在根目录下这是conftest下的内容:
@pytest.fixture(scope='function') def openbrowser(): print("打开浏览器") @pytest.fixture(scope='session') def qingan(): print("我是清安")
这是某个文件内的demo下的内容,主要看Testcase中的内容:
@pytest.fixture(scope='session') def login(): print("登录方法") # return "浏览器" # 此处当scope等于function时,yield可以作用于全局 yield "类似于teardown,用法可以跟return一样" def testSQL(login): print("链接数据库") print(login) return "数据库连接结果" @pytest.fixture() def test01(login): print("用例1") class Testcase(): def test02(self,openbrowser): print("用例2") def test03(self,qingan): print("用例3")
我调用了conftest下的function跟session参数方法。
这里可以看到,都被调用了。当然方法不唯一,用法不唯一,这里做这么一个简单的介绍,主要看各位怎么个用法。module也可以这样写。
接下来我们来看看实际的示例,我打开浏览器:demo.py文件
from selenium import webdriver # 这里每次都需要打开一次浏览器,然后关闭再打开 @pytest.fixture def fox(): fox1 = webdriver.Firefox() print("login") return fox1 def test_seacher(fox): fox.get("https://baidu.com") fox.quit() def test_seacher1(fox): fox.find_element_by_id('kw').send_keys("pytest") fox.find_element_by_id('su').click() fox.quit()
这里是单独的fixture直接同一文件下调用。打开两开浏览器,有点像unittest中的setup用法。这里的test_seacher1`又再次打开了一次浏览器导致不是百度界面无法定位到元素。
那么我们看看session怎么用------conftest.py文件
import pytest from selenium import webdriver @pytest.fixture(scope='session') def fox(): fox = webdriver.Firefox() yield fox
demo.py文件
@pytest.mark.usefixtures('fox') class TestCase(): def test01(self,fox): fox.get("https://baidu.com") print(fox.title) def test02(self,fox): fox.find_element_by_id('kw').send_keys("pytest") fox.find_element_by_id('su').click() def test03(self,fox): fox.quit()
这里用到了mark标签中的usefixtures方法,不用解释应该也知道了有use的地方是什么意思了吧,使用fixture方法,这里直接引入了conftest中的fox,只打开一次浏览器就能往下定位了。是不是很神奇。当然,module也是用在conftest里面。
参数化
有点像数据驱动,可以处理一些测试数据,简单的看看它的一个格式情况:
argnames:参数名
argvalues:参数对应值,类型必须为list
datas = [('qingan', '123'),('qingqingan', '123456')] class TestQingan: @pytest.mark.parametrize(argnames=['username', 'password'], argvalues=datas) def testqing01(self, username, password): # 这里的运行两个用例 print("处处把欢喜") print(username, password)
这里会把datas中的数据解析出来:列表中有几个元组就有几个对应的数据。
当然,还有一种情况就是会随机匹配对应的数值,说的比较抽象我们直接看看:
class TestQingan: @pytest.mark.parametrize("username",["qingan","123456"]) @pytest.mark.parametrize("password",["qingqingan","123456"]) def testqing01(self, username, password): # 这里的运行两个用例 print("处处把欢喜") print(username, password)
如果你这样写了,那么pytest会把username跟password中的列表打乱,然后一一匹配:
命名
不知道你是否喜欢pytest的命名规则,如果不喜欢,此处有一好办法,帮你解决。
跟目录下创建一个pytest.ini文件,在里面这样写。pytest会自动去识别。往下看会解释规则
[pytest] addopts= -s -vv --alluredir=./result/xml testpaths=../P_pytest/inidemo python_files=test*.py python_classes=Qing* python_functions=qing*
以我上述的例子为例题运行出来的效果也与上述没有太大的差别,只是更改了名字而已:
class Qingan: @pytest.mark.parametrize("username",["qingan","123456"]) @pytest.mark.parametrize("password",["qingqingan","123456"]) def qing01(self, username, password): # 这里的运行两个用例 print("处处把欢喜") print(username, password)
好了,下面解释一下pytest.ini里面的配置规则
[pytest] addopts= -s -vv --alluredir=./result/xml addopts= -s -vv --html=./report/test.html # 这里是文件运行的目录../表示上级目录 # 这里是ALLURE测试报告数据存放的地方 addopts= -s -vv --alluredir=./result/xml # 控制台运行输出报告 这里指定生成数据的地方 这里指定输出报告的地方 # allure generate --clean ./result/xml -o ./result/html 这里是在控制台输出的,可别搞混了 # 这里是寻找执行用例所存放的文件 testpaths=../P_pytest/inidemo # 寻找执行用例。这里可更改用例名称,不在是test开头 python_files=test*.py # 寻找对应的类运行 python_classes=Qing* # 基于上述条件寻找对应的用例运行 python_functions=qing*
明白的安排上。前面将了一些allure测试报告的生成,怕你各位没看懂!
注意:pytest.ini中除了必须的配置参数外,不要存在其他的东西,汉字,字符,注释等。
扩展
既然生成了allure报告,为何不好好发挥一下呢,我们往报告里加入截图:
class Qingan: def qing02(self): fox = webdriver.Firefox() try: fox.get('https://baidu.com') time.sleep(3) except Exception as e: """点击错误的时候在报告中插入截图""" file_path = './img/' + str(int(time.time())) + ".png" fox.save_screenshot(file_path) with open(file_path, mode='rb') as file_img: f = file_img.read() # 此处的shouye是放在allure报告中的名字 allure.attach(f, 'shouye', allure.attachment_type.PNG) fox.quit()
代码给到各位,仅限于借鉴参考!