pytest
快速入门
pytest是Python的单元测试框架,同自带的unittest框架类似,但pytest框架使用起来更简洁,效率更高。
pytest特点
- 入门简单易上手,文档支持较好。
- 支持单元测试和功能测试。
- 支持参数化。
- 可以跳过指定用例,或对某些预期失败的case标记成失败。
- 支持重复执行失败的case。
- 支持运行由unittest编写的测试用例。
- 有很多第三方插件,并且可自定义扩展。
- 方便和支持集成工具进行集成。
安装
pip install pytest
注意:你创建的pytest脚本名称中不允许含有.
,比如1.简单上手.py
,这样会报错。当然,可以这么写1-简单上手.py
简单示例
demo1.py
:
import pytest
def run():
assert 1
def test_01():
print('test_01')
assert 1
def test_02():
print('test_02')
assert 1
class TestCS():
def test_case_1(self):
print('第三个')
assert 1
def test_case_2(self):
print('第四个')
assert 1
if __name__ == '__main__':
pytest.main(['-s', '1-简单上手.py'])
上例中,当我们在执行测试用例的时候,pytest.main(["-s", "
1-简单上手.py"])
中的传参需要是一个元组或者列表
返回的结果大致的信息就是告诉我们:
collected 2 items
:本次执行中,收集了4个用例。- 完了开始执行用例,
.
表示执行成功,F
表示执行失败。 - 脚本中的第一个用例执行失败;第二个用例执行成功;但是
run()
并没有执行,由此我们知道,pytest只识别以test_
开头的用例。
pytest.main(["-s", "1-简单上手.py"])参数说明
-s
,在控制台中输出print的结果,有些情况下,ptest不会展示print的输出,所以使用-s
参数来实现。-
1-简单上手.py是要执行的脚本名称。
pytest中的setup和teardown
我们知道,在unittest中,setup和teardown可以在每个用例前后执行,也可以在所有的用例集执行前后执行。那么在pytest中,有以下几种情况:
- 模块级别,也就是在整个测试脚本文件中的用例集开始前后,对应的是:
- setup_module
- teardown_module
- 类级别,在类中的所有用例集执行前后,对应的是:
- setup_class
- teardown_class
- 在类中呢,也可以在进一步划分,在每一个方法执行前后,对应:
- setup_method
- teardown_method
- 函数级别,在用例函数之前后,对应:
- setup_function
- teardown_function
来一一看看各自的用法。
函数级别
# -----------函数级别-----------
def setup_function():
print('每个函数用例执行之前,执行我')
def teardown_function():
print('每个函数用例执行之后,执行我')
def test_fun1():
print('我是用例1')
def test_fun2():
print('我是用例2')
类级别
class TestCas():
def setup_class(self):
print('类中,函数执行之前,执行我,只执行一次')
def teardown_class(self):
print('类中,函数执行之后,执行我,只执行一次')
def test_fun3(self):
print('我是用例3')
def test_fun4(self):
print('我是用例4')
类中方法级别
# -----------类中方法级别-----------
class TestCas2():
def setup_method(self):
print('类中,每个函数执行之前,都执行我')
def teardown_method(self):
print('类中,每个函数执行之后,都执行我')
def test_fun3(self):
print('我是用例3')
def test_fun4(self):
print('我是用例4')
模块级别
def setup_module():
print('模块级别,在所有setup中最先执行')
def teardown_module():
print('模块级别,在所有teardown中最后执行')
if __name__ == '__main__':
pytest.main(['-v', '-s', '2-setup和teardown.py'])
小结
- 在类中,不需要
__init__
方法。 - 测试类的类名必须以
Test
开头。 - 类中的测试方法编写规则跟函数一致。
配置文件
该脚本有多种运行方式,如果处于PyCharm环境,可以使用右键或者点击运行按钮运行,也就是在pytest中的主函数中运行
当然,还有一种是使用配置文件运行,来看看怎么用。
在项目的根目录下,我们可以建立一个pytest.ini
文件,在这个文件中,我们可以实现相关的配置:
[pytest]
addopts = -s -v
testpaths = ./scripts
python_files =test_*.py
python_classes = Test*
python_functions = test_*
xfail_strict=true
那这个配置文件中的各项都是什么意思呢?
首先,pytest.ini
文件必须位于项目的根目录,而且也必须叫做pytest.ini
。
其他的参数:
-
addopts
可以搭配相关的参数,比如-s
。多个参数以空格分割,其他参数后续用到再说。-s
,在运行测试脚本时,为了调试或打印一些内容,我们会在代码中加一些print内容,但是在运行pytest时,这些内容不会显示出来。如果带上-s,就可以显示了。-v
,使输出结果更加详细。
-
testpaths
配置测试用例的目录,- 因为我们用例可能分布在不同的目录或文件中,那么这个
scripts
就是我们所有文件或者目录的顶层目录。其内的子文件或者子目录都要以test_
开头,pytest才能识别到。 - 另外,上面这么写,是从一个总目录下寻找所有的符合条件的文件或者脚本,那么我们想要在这个总目录下执行其中某个具体的脚本文件怎么办?
[pytest] testpaths = ./scripts/ python_files = test_case_01.py
这么写就是执行
scripts
目录下面的test_case_01.py
这个文件。 - 因为我们用例可能分布在不同的目录或文件中,那么这个
-
python_classes
则是说明脚本内的所有用例类名必须是以Test
开头,当然,你也可以自定义为以Test_
开头,而类中的用例方法则当然是以test_
开头。 -
python_functions
则是说脚本内的所有用例函数以test_
开头才能识别。 -
xfail_struct,表示预期失败,结果却是成功的用例,标记为失败(严格模式)
以上配置完毕,在控制台输入pytest即可执行
配置文件的注意事项:
- 'scripts'目录一般跟'pytest.ini'文件同级
- 'pytest.ini'文件中不能有中文,并且放在项目的根目录下面
跳过用例
我们知道在unittest中,跳过用例可以用skip
,那么这同样是适用于pytest。
来看怎么使用:
import pytest
@pytest.mark.skip(reason='xxx')
def test_case_1():
assert 1
@pytest.mark.skipif(condition=1 < 2, reason='跳过哈哈哈')
def test_case_2():
assert 1
if __name__ == '__main__':
pytest.main(['test_skip.py', '-v', '-s'])
跳过用例,我们使用@pytest.mark.skipif(condition, reason)
:
- condition表示跳过用例的条件。
- reason表示跳过用例的原因。
然后将它装饰在需要被跳过用例的的函数上面。
标记预期失败
如果我们事先知道测试函数会执行失败,但又不想直接跳过,而是希望显示的提示。
Pytest 使用 pytest.mark.xfail
实现预见错误功能::
xfail(condiition, reason, [raises=None, run=True, strict=False])
需要掌握的必传参数的是:
- condition,预期失败的条件,当条件为真的时候,预期失败。
- reason,失败的原因。
那么关于预期失败的几种情况需要了解一下:
- 预期失败,但实际结果却执行成功。
- 预期失败,实际结果也执行执行失败。
来看示例:
@pytest.mark.xfail()
def test_case_01():
assert 1
@pytest.mark.xfail()
def test_case_02():
assert 0
def test_case_03():
assert 1
def test_case_04():
assert 0
if __name__ == '__main__':
pytest.main(['5-预期失败.py', '-v', '-s'])
因为一些场景,某个用例我预期它会失败,那就对这个接口标记一下,
-
FAILED : 正常用例失败标识
-
PASSED:正常的用例通过标识
-
XFAIL:预期失败,实际结果也是失败
-
XPASS:预期失败,实际结果是成功
参数化
pytest身为强大的单元测试框架,那么同样支持DDT数据驱动测试的概念。也就是当对一个测试函数进行测试时,通常会给函数传递多组参数。比如测试账号登陆,我们需要模拟各种千奇百怪的账号密码。
当然,我们可以把这些参数写在测试函数内部进行遍历。不过虽然参数众多,但仍然是一个测试,当某组参数导致断言失败,测试也就终止了。
通过异常捕获,我们可以保证程所有参数完整执行,但要分析测试结果就需要做不少额外的工作。
在 pytest 中,我们有更好的解决方法,就是参数化测试,即每组参数都独立执行一次测试。使用的工具就是 pytest.mark.parametrize(argnames, argvalues)
。
- argnames参数名,表示每次从argvalues中提取的一个元素
- argvalues表示要传的数据,如列表,元组,字典,集合
使用就是以装饰器的形式使用。
只有一个参数的测试用例
import pytest
import requests
phone = [10086, 110, 120]
@pytest.mark.parametrize('item', phone)
def test_case1(item): # item命名遵从于argnames参数
print(item)
assert 1
多个参数的测试用例,示例一:
a = [10086, 110, 120]
b = ['10086', '110', '120']
# 加两个装饰器效果,a列表中每个元素都会b列表中的每个元素匹配一遍
@pytest.mark.parametrize('item', a)
@pytest.mark.parametrize('code', b)
def test_case1(item, code): # item命名遵从于argnames参数
print(item,code)
assert 1
这个结果很有意思,它执行了9次
因为加了2个装饰器,a列表中每个元素都会b列表中的每个元素匹配一遍,而这应该不是我们想要的结果,我们需要的是一一对应,我看下示例2
多个参数的测试用例,示例二:
test1 = [10086, 110, 120]
test2 = ['10086', '110', '120']
print(zip(test1, test2))
# --对应,使用zip()
@pytest.mark.parametrize('test1,test2', zip(test1, test2))
def test_case1(test1, test2): # item命名遵从于argnames参数
print(test1, test2)
assert 1
在看下运行结果
没错,使用zip()函数,这个正是我们想要的结果
固件
什么是固件
固件(Fixture)是一些函数,pytest 会在执行测试函数之前(或之后)加载运行它们,也称测试夹具。
我们可以利用固件做任何事情,其中最常见的可能就是数据库的初始连接和最后关闭操作。
Pytest 使用 pytest.fixture()
定义固件,下面是最简单的固件,访问主页前必须先登录:
import pytest
"""固件,又称测试夹具,类似setup/teardown的功能,但是相对比较灵活"""
@pytest.fixture()
def login():
print('登录成功')
def test_index(login):
# 该用例执行之前需要登录
print('欢迎来到主页')
assert 1
作用域
在之前的示例中,你可能会觉得,这跟之前的setup和teardown的功能也类似呀,但是,fixture相对于setup和teardown来说更灵活。pytest通过scope
参数来控制固件的使用范围,也就是作用域。
在定义固件时,通过 scope
参数声明作用域,可选项有:
function
: 函数级,每个测试函数都会执行一次固件;class
: 类级别,每个测试类执行一次,所有方法都可以使用;module
: 模块级,每个模块执行一次,模块内函数和方法都可使用;session
: 会话级,一次测试只执行一次,所有被找到的函数和方法都可用。
默认的作用域为
function
。
预处理和后处理
很多时候需要在测试前进行预处理(如新建数据库连接),并在测试完成进行清理(关闭数据库连接)。
当有大量重复的这类操作,最佳实践是使用固件来自动化所有预处理和后处理。
Pytest 使用 yield
关键词将固件分为两部分,yield
之前的代码属于预处理,会在测试前执行;yield
之后的代码属于后处理,将在测试完成后执行。
以下测试模拟数据库查询,使用固件来模拟数据库的连接关闭
@pytest.fixture()
def db():
# test_request 执行之前
print('connect db ...')
yield
# test_request 执行之后
print('close db ...')
def test_request(db):
print('欢迎来到主页')
assert 1
if __name__ == '__main__':
pytest.main(['-v', '-s', '7-fixture.py'])
来看下运行结果
失败重跑
失败重跑 - rerun
当用例执行失败后,可以指定重新运行的次数,常用来处理网络不佳的情况下.
下载
pip install pytest-rerunfailures
示例
import pytest
import random
"""
失败重跑 - rerun
当用例执行失败后,可以指定重新运行的次数,常用来处理网络不佳的情况下.
使用:在配置文件ini中的'addopts参数后跟 --reruns=3'
"""
def test_random():
if random.randint(0, 1):
assert 1
else:
assert 0
if __name__ == '__main__':
pytest.main(['-v', '-s', '--reruns=3', '8-失败重跑.py', ])
控制用例的执行顺序
-
pytest-ordering 控制用例的执行顺序
- 在pytest中,用例的执行顺序是从上到下的形式,如果有需要自定义控制用例执行顺序,用ordering,
- 如果有排序的和没有排序的用例 同时存在,先执行有排序的,再执行没有排序的(从上到下依次执行)
下载
pip install pytest-ordering
示例
import pytest """ pytest-ordering 控制用例的执行顺序 在pytest中,用例的执行顺序是从上到下的形式,如果有需要自定义控制用例执行顺序,用ordering 如果有排序的和没有排序的用例 同时存在,先执行有排序的,再执行没有排序的(从上到下依次执行) """ class TestCase(object): @pytest.mark.run(order=3) def test_case_01(self): print('执行测试用例1') @pytest.mark.run(order=1) def test_case_02(self): print('执行测试用例2') @pytest.mark.run(order=0) def test_case_03(self): print('执行测试用例3') if __name__ == '__main__': pytest.main(['-v', '-s', '9-控制用例的执行顺序.py'])
HTML报告
下载
pip install pytest-html
使用:
-
在配置文件ini中addopts参数后跟 --html=./report.html
import pytest """ pytest-html 使用:在配置文件ini中addopts参数后跟 --html=./report.html """ class TestCase(object): @pytest.mark.run(order=3) def test_case_01(self): print('执行测试用例1') @pytest.mark.run(order=1) def test_case_02(self): print('执行测试用例2') def test_case_03(self): print('执行测试用例3') if __name__ == '__main__': pytest.main(['-v', '-s', '10-html报告.py','--html=./report.html'])
allure报告
Allure框架是一个灵活的轻量级多语言测试报告工具,它不仅以web的方式展示了简洁的测试结果,而且允许参与开发过程的每个人从日常执行的测试中最大限度的提取有用信息。
从开发人员(dev,developer)和质量保证人员(QA,Quality Assurance)的角度来看,Allure报告简化了常见缺陷的统计:失败的测试可以分为bug和被中断的测试,还可以配置日志、步骤、fixture、附件、计时、执行历史以及与TMS和BUG管理系统集成,所以,通过以上配置,所有负责的开发人员和测试人员可以尽可能的掌握测试信息。
从管理者的角度来看,Allure提供了一个清晰的“大图”,其中包括已覆盖的特性、缺陷聚集的位置、执行时间轴的外观以及许多其他方便的事情。allure的模块化和可扩展性保证了我们总是能够对某些东西进行微调。
少扯点,来看看怎么使用。
Python的pytest中allure下载
pip install allure-pytest
但由于这个allure-pytest
插件生成的测试报告不是html
类型的,我们还需要使用allure工具再“加工”一下。所以说,我们还需要下载这个allure工具。
allure工具下载
在现在allure工具之前,它依赖Java环境,我们还需要先配置Java环境。
http://www.oracle.com/technetwork/java/javase/downloads/index.html
注意,如果你的电脑已经有了Java环境,就无需重新配置了。
配置完了Java环境,我们再来下载allure工具
https://github.com/allure-framework/allure2
下载并解压好了allure工具包之后,还需要将allure包内的bin
目录添加到系统的环境变量中。
完事后打开你的终端测试:
返回了版本号说明安装成功。
使用
一般使用allure要经历几个步骤:
- 配置
pytest.ini
文件。 - 编写用例并执行。
- 使用allure工具生成html报告。
来看配置pytest.ini
[pytest] addopts = -v -s --html=report/report.html --alluredir ./report/allure_result testpaths = ./scripts/ python_files = test_allure_case.py python_classes = Test* python_functions = test_* # xfail_strict=true
示例
import pytest import allure """ allure是第三方轻量级的测试报告插件 - 易安装 - 界面优美 - 可配置性强 allure依赖java环境,需要安装java环境 allure插件下载 allure插件安装:解压压缩包到没有一个中文的目录中,然后将其中的bin目录添加到环境变量(PATH)中 allure测试:在cmd终端中输入allure --version python 安装allure 模块 python通过allure模块生成allure需要的中间数据 - json数据 在使用allure插件读取json数据,进而生成allure测试报告 下载allure模块 pip install allure-pytest 使用: 在配置文件ini中addopts参数后跟--alluredir ./report/allure_result 在当前的report目录下生成allure_result目录以及相关json数据 使用allure插件读取json数据,生成allure报告 allure generate ./report/allure_result -o ./report/allure_html 如果报告已存在,再次生成加上 --clean allure generate ./report/allure_result -o ./report/allure_html --clean 注意:allure报告展示需要http服务器的支持,所以,目前来说,有两种打开方式 1.pycharm直接打开 2.open命令打开 allure open ./allure_html # allure_html目录是allure插件生成的allure报告目录 """ class TestCase(object): @pytest.mark.run(order=3) def test_case_01(self): print('执行测试用例1') @pytest.mark.run(order=1) def test_case_02(self): print('执行测试用例2') def test_case_03(self): print('执行测试用例3') if __name__ == '__main__': pytest.main(['-v', '-s', '11-allure报告.py', '--alluredir ./report/allure_result'])
allure高级用法
- title,自定义用例标题,标题默认是用例名。
- description,测试用例的详细说明。
- feature和story被称为行为驱动标记,因为使用这个两个标记,通过报告可以更加清楚的掌握每个测试用例的功能和每个测试用例的测试场景。或者你可以理解为feature是模块,而story是该模块下的子模块。
- allure中对bug的严重(severity)级别也有定义,allure使用
severity
来标识测试用例或者测试类的bug级别,分为blocker,critical,normal,minor,trivial5个级别。一般,bug分为如下几个级别:- Blocker级别:中断缺陷(客户端程序无响应,无法执行下一步操作),系统无法执行、崩溃或严重资源不足、应用模块无法启动或异常退出、无法测试、造成系统不稳定。
- Critical级别:即影响系统功能或操作,主要功能存在严重缺陷,但不会影响到系统稳定性。比如说一个服务直接不可用了,微信不能发消息,支付宝不能付款这种,打开直接报错。
- Major:即界面、性能缺陷、兼容性。如操作界面错误(包括数据窗口内列名定义、含义是否一致)、长时间操作无进度提示等。
- Normal级别:普通缺陷(数值计算错误),是指非核心业务流程产生的问题,比如说知乎无法变更头像,昵称等。这个要看自己的定义。
- Minor/Trivial级别:轻微缺陷(必输项无提示,或者提示不规范),比如各种影响体验,但不影响使用的内容。
- dynamic,动态设置相关参数。
allure.title与allure.description
import allure
import pytest
import requests
@allure.title('第一个测试用例')
@allure.description('今天有点饿了') # 描述
def test_case01():
assert 1
@allure.title('第二个测试用例')
@allure.description('天气不错')
def test_case02():
assert 1
feature和story
@allure.feature('注册用例') class TestRegister(object): @allure.story('注册用例1') @allure.title('注册用例1') def test_case_1(self): assert 1 @allure.story('注册用例2') @allure.title('注册用例2') def test_case_2(self): assert 1 @allure.story('注册用例3') @allure.title('注册用例3') def test_case_3(self): assert 1
由上图可以看到,不同的用例被分为不同的功能中。
allure.severity
allure.severity
用来标识测试用例或者测试类的级别,分为blocker,critical,normal,minor,trivial5个级别。
import pytest import allure @allure.feature('登录模块') class TestCaseLogin(object): @allure.severity(allure.severity_level.BLOCKER) def test_case_01(self): assert 1 @allure.severity(allure.severity_level.CRITICAL) def test_case_02(self): assert 1 @allure.severity(allure.severity_level.MINOR) def test_case_03(self): assert 1 @allure.severity(allure.severity_level.TRIVIAL) def test_case_04(self): assert 1 def test_case_05(self): assert 1
severity的默认级别是normal,所以上面的用例5可以不添加装饰器了。
allure.dynamic动态属性
- 在参数化的时候,我们无法使用allure的相关功能,如果数据都在用例中,所以我们采用allure.dynamic.动态为用例添加相关功能
- 取请求体的数据,作allure报告中标题,描述等等
- feature 模块
- allure.dynamic.feature(feature_name)
- 功能点 story
- allure.dynamic.story(case_story)
- 用例标题 title
- allure.dynamic.title(case_title)
url_list = [ {"url": "http://www.baidu.com", "method": "get", "status": 200}, {"url": "https://www.cnblogs.com/TodayWind/p/14158239.html", "method": "get", "status": 200}, ] @pytest.mark.parametrize('item', url_list) def test_case(item): response = requests.request(method=item['method'], url=item['url']) response.encoding = 'utf-8' from bs4 import BeautifulSoup soup = BeautifulSoup(response.text, 'html.parser') title = str(soup.find(name='title')) allure.dynamic.title(title) # # 取请求体的数据,作allure报告中标题,描述等等 assert response.status_code == item['status']