pytest用例管理框架(单元测试框架)
主要作用
- 发现测试用例:从多个py文件中按照一定的规则找到测试用例
- 执行测试用例:按照一定的顺序执行测试用例,生成测试结果
- pytest:默认从上到下,可以使用装饰器改变规则
- unittest:默认按照ASCII顺序执行
- 判断测试结果:断言
- 生成测试报告:pytest-html,allure报告
pytest简介
基于python的单元测试框架,他可以和selenium、requests,appium结合实现自动化测试
实现用例跳过skip和reruns失败用例重跑
可以结合allure-pytest插件生成allure报告
很方便和jenkins实现持续集成
有很多强大的插件:
- pytest-html:生成html测试报告
- pytest-xdist:多线程执行测试用例
- pytest-ordering:改变测试用例的执行顺序
- pytest-rerunfaiures:失败用例重跑
- allure-pytest:生成allure报告
requirements.txt文档
1 | pytest==7.2.0 |
pip install -r requirements.txt
pytest的最基本的测试用例规则
- 模块名必须以test_开头或者_test结尾
- 测试类必须以Test开头,并且不能带有init方法
- 测试用例必须以test_开头
命名规范:
- 模块名:一般全小写,多个英文之间用_隔开
- 类名:类名一般是首字母大写
- 方法名:一般全小写,多个英文之间用_隔开
运行方式
主函数方式
1 | import pytest |
常见参数:
- -v:输出更加详细的信息,比如文件和用例的名称等
- -s:输出调试信息,打印信息等
- –reruns:失败重跑
- -x:出现一个失败用例就停止测试
- –maxfail=2:出现N个失败用例才终止测试
- –html=report.html:生成html的测试报告
- -n=2:N个线程执行
- -k:运行测试用例名称中包含执行字符串的用例
- pytest.main([‘-vs’,’-k’,’mengxun or william’])
- pytest.main([‘-vs’,’-k’,’mengxun and william’])
指定模块运行
1 | # 仅运行testcases文件夹下的test_api.py文件 |
指定文件夹
1 | # 运行testcases文件夹下的所有test文件 |
通过node id的方式运行测试用例
1 | # 仅执行testcases文件夹下test_api.py文件中的Test_product_manage_1用例 |
通过pytest.ini的配置文件运行
不管是命令行还是主函数都会读取这个配置文件
1 | [pytest] |
用例里面添加标记 @pytest.mark.smoke
1 | class Test_login(): |
执行的时候通过-m参数指定标记
addopts = ‐vs ‐m smoke
命令行方式
pytest
pytest -vs -n=2 ./testCase/test_login.py -m "smoke or userManage or productManage"
pytest -vs -n=2 ./testCase/test_login.py:test_01
pytest默认的执行测试用例顺序
pytest的默认执行顺序是从上到下的
改变默认用例的执行顺序,在用例上添加标记:@pytest.mark.run(order=1)
注意:有order装饰器的优先,相同的从上到下,然后再是没有装饰器的,负数不起作用。
跳过测试用例
无条件跳过
1
2
3
4
def test_02(self):
time.sleep(0.5)
print("测试test_02")有条件跳过
1
2
3
4
5
6age = 19
def test_04(self):
time.sleep(0.5)
print("测试test_04")
setup/teardown, setup_class/teardown_class
为什么需要这些功能?
比如:web自动化执行用例之前,请问需要打开浏览器吗?用例执行后需要关闭浏览器吗?
1 | import pytest |
注意:和Unittest不一样,全是小写
使用fixture装饰器来实现部分用例的前后置
可以指定部分用例执行setup或者teardown
@pytest.fixture(scope="", params="", autouse="", ids="", name="")
scope: 表示的是被@pytest.fixture标记的方法的作用域。function(默认), class, module, package/session
function: 在每个 def 前后执行一次。
@pytest.fixture(scope="function")
class: 在每个 class 前后执行一次。
@pytest.fixture(scope="class")
module: 在每个模块前后执行一次。
@pytest.fixture(scope="module")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def my_fixture():
print("这是前置方法")
yield
print("这是后置方法")
class TestInterface():
def test_addition(self):
assert add(3, 5) == 8
assert add(-1, 1) == 0
assert add(-1, -1) == -2
def test_subtraction(self, my_fixture):
assert subtract(5, 3) == 2
assert subtract(-1, 1) == -2
assert subtract(-1, -1) == 0params: 参数化(支持:列表[],元组(),字典列表[{},{},{}],字典元组({},{},{}))
request 和 request.param 是 pytest 的固定写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def my_fixture(request):
# return request.param
print("前置")
yield request.param # yield 后面返回可以接代码
print("后置")
class TestInterface():
# 测试加法函数
def test_addition(self):
assert add(3, 5) == 8
assert add(-1, 1) == 0
assert add(-1, -1) == -2
def test_subtraction(self, my_fixture):
print("\n--------------------")
print(str(my_fixture))
print("--------------------\n")
assert subtract(5, 3) == 2
assert subtract(-1, 1) == -2
assert subtract(-1, -1) == 0结果
autouse: 自动执行,默认False。搭配 scope 一起使用,可实现全部执行前后置
1
2
3
4
5
6
7
8
9
10
11
12
13
def my_fixture():
print("这是前置方法")
yield
print("这是后置方法")
class TestInterface():
def test_addition(self):
assert add(3, 5) == 8
def test_subtraction(self):
assert subtract(5, 3) == 2ids: 当使用 params 参数化时,给每一个值设置一个变量名。意义不大。
params 参数化那张图片的红框里面的 unicode 编码,我们可以使用 ids 来进行替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
)
def my_fixture(request):
# return request.param
print("前置")
yield request.param
print("后置")
class TestInterface():
def test_addition(self):
assert add(3, 5) == 8
def test_subtraction(self, my_fixture):
print("\n--------------------")
print(str(my_fixture))
print("--------------------\n")
assert subtract(5, 3) == 2结果
name: 给被 @pytest.fixture 标记的方法取一个别名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
)
def my_fixture(request):
# return request.param
print("前置")
yield request.param
print("后置")
class TestInterface():
def test_addition(self):
assert add(3, 5) == 8
assert add(-1, 1) == 0
assert add(-1, -1) == -2
def test_subtraction(self, abc):
print("\n--------------------")
print(str(abc))
print("--------------------\n")
assert subtract(5, 3) == 2
assert subtract(-1, 1) == -2
assert subtract(-1, -1) == 0- 注意:fixture将方法起别名后,不能再引用原本方法名了
通过conftest.py和pytest.fixtrue()结合使用实现全局的前置应用
比如:项目的全局登录,模块的全局处理
- conftest.py文件是单独存放的一个夹具配置文件,名称不能更改
- 用处:可以在不同的py文件中使用同一个fixtrue函数
- 原则上conftest.py需要和运行的用例放到统一层,并且不需要做import导入操作
1 | # 目录结构 |
下面是三个 conftest.py 文件,节省空间写到一段里了
1 | # 作用于interfaceCase文件夹下的全局文件 |
如何引用 conftest.py 文件呢?
1 | import time |
结果
总结:
- setup/teardown,setup_class/teardown_class 它作用于所有用例或者所有的类
- @pytest.fixtrue() 它的作用既可以部分,也可以全部前后置
- conftest.py 和 @pytest.fixture() 结合使用,作用于全局的前后置
断言
assert
1 | class TestInterface(): |
pytest结合allure-pytest插件生成allure测试报告
- 比较丑的报告:pytest-html
- 比较好看的报告:allure-pytest
配置allure环境
- 下载 allure ZIP包
https://github.com/allure-framework/allure2/releases
pip install pytest-allure
vim ~/.zshrc
并在最后一行添加环境变量export PATH="/Users/william/DYJ/Tools/allure-2.7.0/bin:$PATH"
- 使环境变量生效
source ~/.zshrc
- 验证
allure --version
生成allure报告
修改
pytest.ini
文件中的addopts
参数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20[pytest]
#--html ./report/report.html,reruns 失败重跑次数
# addopts = -vs -n=2 -reruns 2 --html ./report/report.html
# 生成json格式的allure报告
addopts = -vs --alluredir ./temp
# 指定 pytest 搜索测试文件的路径。pytest 将在 ./interfaceCase 目录下搜索可执行文件
testpaths =
./interfaceCase
./testCase
# 指定 pytest 应该匹配的测试文件模式。pytest 将会运行所有以 test_ 为开头的 python 文件
python_files = test_*.py
# 指定 pytest 应该匹配的测试类名模式。pytest 将会运行所有以 Test 开头的类
python_classes = Test
# 指定 pytest 应该匹配的测试函数模式。pytest 将会运行所有以 Test 开头的函数
python_functions = test
# 用于为测试用例添加标记。使用方法: @pytest.mark.smoke pytest -m "smoke [or usermanage]"
markers =
smoke : Smoke Testing Module
userManage : User Management Module
productManage : 商品管理修改 mian.py 文件
1
2
3
4
5
6import os
import pytest
if __name__ == '__main__':
pytest.main()
os.system('allure generate ./temp -o ./report --clean')allure generate
固定写法./temp
: 临时的 json 格式报告的路径-o
: 输出 output./report
: 生成的 allure 报告的路径--clean
: 清空./report
路径原来的报告
执行main.py文件
python3 main.py
@pytest.mark.parametrize数据驱动
@pytest.mark.parametrize(args_name, args_value)
args_name:
参数名args_value:
参数值,有多少值就会执行多少次用例。(可以为:列表,元组,字典列表,字典元组)第一种方式
1
2
3
4
5
6
7
8
9import pytest
class TestApi():
def test_api_01(self, args):
print(args)
if __name__ == "__main__":
pytest.main()第二种方式:跟unittest的ddt里面的@unpack解包一样
1
2
3
4
5
6
7
8
9import pytest
class TestApi():
def test_api_02(self, name, age):
print(name, age)
if __name__ == "__main__":
pytest.main()
结果
YAML语法
yaml文件的基础语法在这篇文章YAML语法中有写,这里就不在赘述了
如何使用yaml文件呢?
安装pyyaml,
pip install pyyaml
编写yaml文件
1
2
3mengxun:
- name: 小苍鹰
- age: 25调用yaml文件,并打印
1
2
3
4
5
6
7
8
9
10
11
12
13
14import yaml
class YamlUntil():
def __init__(self, yaml_file):
self.yaml_file = yaml_file
# 读取yaml文件
def read_yaml(self):
with open(self.yaml_file, 'r', encoding='utf-8') as f:
data = yaml.load(f, Loader=yaml.FullLoader)
print(data)
if __name__ == '__main__':
YamlUntil('yaml_until.yaml').read_yaml()运行yaml_until.py文件
结果
yaml接口自动化练习
编写yaml文件,包含用例名称、请求头,请求体
一条搜索新闻,一条搜索B站
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26# test_api.yaml
-
name: bing搜索keywords
request:
url: https://cn.bing.com/search
method: get
headers:
Content-Type: application/json
params:
go: 搜索
q: 今日新闻
# validate:
# - eq: {returnValue: 200}
-
name: bing搜索keywords
request:
url: https://cn.bing.com/search
method: get
headers:
Content-Type: application/json
params:
go: 搜索
q: B站
# validate:
# - eq: {returnValue: 200}编写类方法,读取yaml文件
1
2
3
4
5
6
7
8
9
10
11import yaml
class YamlUntil():
def __init__(self, yaml_file):
self.yaml_file = yaml_file
# 读取yaml文件
def read_yaml(self):
with open(self.yaml_file, 'r', encoding='utf-8') as f:
data = yaml.load(f, Loader=yaml.FullLoader) # 加载yaml
return data使用装饰器parametrize
test_api.yaml
文件的返回格式是:{'name': 'bing搜索keywords', 'request': {'url': 'https://cn.bing.com/search', 'method': 'get', 'headers': {'Content-Type': 'application/json'}, 'params': {'go': '搜索', 'q': '今日新闻'}}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import os
import pytest
import requests
import yaml
class YamlUntil():
def __init__(self, yaml_file):
...
return data
class TestYamlApi():
# os.getcwd() 获取当前文件路径,执行的时候,一直说找不到yaml文件
def test_yaml_01(self):
# print(args) # 打印出yaml返回格式
name = args['name']
url = args['request']['url']
params = args['request']['parmas']
res = requests.get(url, params=params)
assert res.status_code == 200
print('\nname: ' + name)
print(res.text[:100])
if __name__ == '__main__':
# YamlUntil('yaml_until.yaml').read_yaml()
TestYamlApi().test_yaml_01()结果