pytest命名规则
- 文件名
- 类
- 方法函数
- 谷歌风格命名规范Google Python命名规范-易微帮
pytest测试用例编写规则
类型 | 规则 |
文件 | test_开头或者_test结尾 |
类 | Test开头 |
方法/函数 | test开头 |
测试类中不可添加__init__构造函数 |
pycharm配置与界面化运行
- 首先进行添加pytest环境
- 其次在settings中的搜索框搜索pytest在default test runner中选择pytest
pytest用例结构
用例结构(缺一不可)
- 用例名称
- 用例步骤
- 用例断言
def test_XXX(self):
# 测试步骤1
# 测试步骤2
# 断言 实际结果 对比 预期结果
assert ActualResult == ExpectedResult
类级别的用例示例
class TestXXX:
def setup(self):
# 资源准备
pass
def teardown(self):
# 资源销毁
pass
def test_XXX(self):
# 测试步骤1
# 测试步骤2
# 断言 实际结果 对比 预期结果
assert ActualResult == ExpectedResult
用例断言
概念:断言(assertion)是一种在程序中的一阶逻辑(如:一个结果为真或假的逻辑判断式),目的为了表示与验证软件开发者预期的结果——当程序执行到断言的位置时,对应的断言应该为真。若断言不为真时,程序会中止执行,并给出错误信息
- 断言写法
assert <表达式>
assert <表达式>,<描述>
示例1
def test_sum():
a = 1
b = 2
expect = 3
assert a + b == expect,f'{a} + {b} = {expect} ,结果为真'
示例2//查看当前环境下是否为linux环境
import sys
def test_plat():
assert ('linux' in sys.platform), "该代码只能在 Linux 下执行"
pytest测试结构框架(set up/tear down)
- set up为一些准备的前置工作
- tear down为完成一些后置工作
类型 | 规则 |
---|---|
setup_module/teardown_module | 全局模块级 |
setup_class/teardown_class | 类级,只在类中前后运行一次(一般常用) |
setup_function/teardown_function | 函数级,在类外 |
setup_method/teardown_method | 方法级,类中的每个方法执行前后 |
setup/teardown | 在类中,运行在调用方法的前后(一般常用) |
def test_case1():
print('case1')
def setup_function():
print("资源准备,setup function")
def teardown_function():
print('资源已损毁 teardown_function')
运行代码后,在终端开启两个button,一个show passed和一个show ignored,如下图
def setup_module():
print('资源准备,setup module')
def teardown_module():
print('资源销毁,teardown_module')
def test_case1():
print('case1')
def test_case2():
print('case2')
def setup_function():
print("资源准备,setup function")
def teardown_function():
print('资源已损毁 teardown_function')
运行结果是:
pytest_study.py::test_case1 资源准备,setup module //module级别的只执行一次
资源准备,setup function //一般每执行一次测试用例就会执行一次
PASSED [ 50%]case1
资源已损毁 teardown_function
pytest_study.py::test_case2 资源准备,setup function
PASSED [100%]case2
资源已损毁 teardown_function
资源销毁,teardown_module
============================== 2 passed in 0.01s ===============================
pytest 参数化
- 通过参数的方式传递数据,从而实现数据和脚本分离
- 并且可以实现用力的重复生成与执行
参数化实现方案
装饰器:@pytest.mark.parametrize
测试登陆场景
- 测试登陆成功,登陆失败(账号错误,密码错误)
- 创建多种账号,中文文本账号,英文文本账号
不使用pytest
def test_param_login_ok():
# 登录成功
username = "right"
password = "right"
login(username,password)
def test_param_login_fail():
# 登录失败
username = "wrong"
password = "wrong"
login(username,password)
使用pytest的场景
@pytest.mark.parametrize("username,password",[["right","right"],["wrong","wrong"]])
def test_param(username,password):
login(username,password)
参数化测试函数使用
- 单参数
- 每一条测试数据就会生成一个测试用例
-
search_list = ['appium', 'selenium', 'pytest'] @pytest.mark.parametrize('search_key', ['appium', 'pytest', 'allure', 'abc']) def test_search_param(search_key): assert search_key in search_list
- 多参数
# 第二种:多参数情况,用列表嵌套列表的方式
@pytest.mark.parametrize("username,password", [["rightusername", "right username"],
["wrong username", "wrong password"],
[" ", "password"]
])
def test_login(username, password):
print(f"登陆的用户名:{username},登陆的密码:{password}")
默认的命名方式测试数据用“-”相连接 如“pytest.pytest_study.test_login”
- 用例重命名
# 第二种:多参数情况,用列表嵌套列表的方式
@pytest.mark.parametrize("username,password", [["rightusername", "right username"],
["wrong username", "wrong password"],
[" ", "password"]
],
ids = ["正常的用户和正确的密码","错误的用户名和错误的密码","用户名为空"]
)
def test_login(username, password):
print(f"登陆的用户名:{username},登陆的密码:{password}")
#第三种给我们的用例进行重命名,默认的命名方式测试数据用“-”相连接 如“pytest.pytest_study.test_login”
如上这样我们的运行结果是不支持中文的,如下为运行后的结果
如想要其支持中文的话,需要在文件夹内添加conftest.py
# 创建conftest.py 文件 ,将下面内容添加进去,运行脚本
def pytest_collection_modifyitems(items):
"""
测试用例收集完成时,将收集到的用例名name和用例标识nodeid的中文信息显示在控制台上
"""
for i in items:
i.name=i.name.encode("utf-8").decode("unicode_escape")
i._nodeid=i.nodeid.encode("utf-8").decode("unicode_escape")
如上运行结果,测试用例将被重命名为中文的格式
- 笛卡尔积(在接口测试中应用比较广)
- 两组数据
- a = [1,2,3]
- b = [a,b,c]
- 对应有几种组形式
- (1,a)(1,b)(1,c)
- (2,a)(2,b)(2,c)
- (3,a)(3,b)(3,c)
-
@pytest.mark.parametrize("b", ["a", "b", "c"]) @pytest.mark.parametrize("a", [1, 2, 3]) def test_param1(a,b): print(f'笛卡尔积形式的参数中a = {a},b = {b}')
测试结果如下:
pytest标记测试用例
pytest设置跳过及预期失败测试用例
- 调试时不想运行这个用例
- 标记无法在某些平台上运行的测试功能
- 在某些版本中执行,其他版本中跳过
- 比如:当前的外部资源不可用时跳过
- 如果测试数据是从数据库中取到的,
- 连接数据库的功能如果返回结果未成功就跳过,因为执行也都报错
- 解决 1:添加装饰器
无条件跳过@pytest.mark.skip
使用方法:@pytest.mark.skip
标记在需要跳过的测试用例上。-
@pytest.mark.skip def test_login(): print('该条测试用例未开发完成') assert True
有条件跳过@pytest.mark.skipif(reason = 'xxx')//需要添加具体的原因
@pytest.mark.skipif(reson = '代码未实现') def test_bbb(): print('该条测试用例,代码未实现') assert False
- 执行完成后:
- 解决 2:代码中添加跳过代码
pytest.skip(reason)
-
def check_login(): return True def test_function(): print('start') # 如果未登陆则跳过后续步骤 if not check_login(): pytest.skip('unsupported configuration') print('end')
- 在mac上跳过,在windows上运行
-
import sys import pytest print(sys.platform) @pytest.mark.skipif(sys.platform == 'darwin',reason='does not run on mac') def test_aaa(): assert True @pytest.mark.skipif(sys.platform == 'win',reason='does not run on windows') def test_bbb(): assert True
- xfail使用场景:
- 与skip类似,预期结果为fail,标记用例为fail
- 用法:添加装饰器@pytest.mark.xfail
pytest运行用例
- 运行 某个/多个 用例包
- 运行 某个/多个 用例模块
- 运行 某个/多个 用例类
- 运行 某个/多个 用例方法
pytest只执行上次失败的测试用例
--lf(--last-failed)
只重新运行故障。--ff(--failed-first)
先运行故障然后再运行其余的测试- pytest文档26-运行上次失败用例(--lf 和 --ff)_weixin_34292959的博客-CSDN博客
常用命令行
python执行pytest代码
- 使用main函数
- 使用python -m pytest 调用pytest(jeckins持续集成)
if __name__ == '__main__': # 1、运行当前目录下所有符合规则的用例,包括子目录(test_*.py 和 *_test.py)
pytest.main() # 2、运行test_mark1.py::test_dkej模块中的某一条用例 pytest.main(['test_mark1.py::test_dkej','-vs']) # 3、运行某个 标签 pytest.main(['test_mark1.py','-vs','-m','dkej']) 运行方式 `python test_*.py `
常用的异常处理方法
Pytest如何对捕获的异常的类型和内容进行断言_pytest捕获异常_redrose2100的博客-CSDN博客
pytest结合数据驱动-yaml
什么是数据驱动
- 数据驱动就是数据的改变从而驱动自动化测试的执行,最终引起测试结果的改变,简单来说,就是参数化的应用,
- 数据量小的测试用例可以使用代码的参数化来实现数据驱动,
- 数据量大的情况下使用结构化的文件(例如yaml,json)来对数据进行存储,然后用测试用例读取这些数据
- 应用:
- app、web、接口自动化测试
- 测试步骤的数据驱动
- 测试数据的数据驱动
- 配置的数据驱动
yaml文件介绍
- 对象:键值对的集合,用冒号“:”表示
- 数组:一组按次序排列的值,前加 “-”
- 纯量:单个的,不可再分的值
- 字符串
- 布尔值
- 整数
- 浮点数
- NULL
- 时间
- 日期
# 编程语言
languages: # 相当于是一对键值 languages:["php","Java","Python"]
- PHP
- Java
- Python
book:
Python入门: # 书籍名称
price: 55.5
author: Lily
available: True
repertory: 20
date: 2018-02-17
Java入门:
price: 60
author: Lily
available: False
repertory: Null
date: 2018-05-11
yaml文件使用
- 查看yaml文件
- pycharm
- txt记事本
- 读取yaml文件
- 安装:pip install pyyaml
- 方法:yaml.safe_load(f) #将yaml转化为python对象
- 方法:yaml.safe_dump(f)#将python对象转化为yaml文件
-
import yaml file_path = './my.yaml' with open(file_path, 'r', encoding='utf-8') as f: data = yaml.safe_load(f)
工程目录文件
- data 目录:存放 yaml 数据文件
- func 目录:存放被测函数文件
- testcase 目录:存放测试用例文件
ps:创建data时,可以创建为文件夹,也可以创建为packages
operation.py#被测函数
def my_add(x, y):
result = x + y
return result
test_add.py#测试case
import pytest
from data_driver_yaml.func.operation import my_add#如果不写这个会造成 下面的my_add函数找不到,会报错
class TestWithYAML:
@pytest.mark.parametrize('x,y,expected', [[1, 1, 2],[1,2,3],[5,6,7]])
def test_add(self, x, y, expected):
assert my_add(int(x), int(y)) == int(expected)
当测试数据量很大时,我们可以从yaml中再读取出来数据
import pytest
import yaml
from data_driver_yaml.func.operation import my_add
#windows电脑需要将with open中再添加一个参数 encoding = “utf-8”
def get_data():
with open("../data/data.yaml") as f:
data = yaml.safe_load(f)
return data
def test_get_data():
print(get_data())
class TestWithYAML:
@pytest.mark.parametrize('x,y,expected', get_data())
def test_add(self, x, y, expected):
assert my_add(int(x), int(y)) == int(expected)
data_yaml#测试数据
#嵌套三个列表[[1,1,2],[3,6,9],[100,200,300]]
-
- 1
- 1
- 2
-
- 3
- 6
- 9
-
- 100
- 200
- 300
pytest结合数据驱动Excel
-
第三方库
xlrd
xlwings
pandas
-
openpyxl
- 官方文档: https://openpyxl.readthedocs.io/en/stable/
- openpyxl库的安装
- 安装:pip install openpyxl
- 导入:import openpyxl
import openpyxl
# 获取工作簿
book = openpyxl.load_workbook('../data/params.xlsx')
# 读取工作表
sheet = book.active
# 读取单个单元格
cell_a1 = sheet['A1']
cell_a3 = sheet.cell(column=1, row=3) # A3
# 读取多个连续单元格
cells = sheet["A1":"C3"]
# 获取单元格的值
cell_a1.value
注意xlsx文件必须用wps进行创建,在文件下创建file重命名为xlsx文件会出现报错的问题
testcases.py文件
from openpyxl_study.read_excel.func.operation import my_add
import pytest
import openpyxl
def get_excel():
book = openpyxl.load_workbook("../data/params.xlsx")
sheet = book.active
cells = sheet["A1":"C3"]
print(cells)
values = []
for row in cells:
data = []
for cell in row:
data.append(cell.value)
values.append(data)
return values
class TestWithEXCEL:
@pytest.mark.parametrize('x,y,expected', get_excel())
def test_add(self, x, y, expected):
assert my_add(int(x), int(y)) == int(expected)
pytest结合csv实现数据驱动
csv文件介绍
- csv:逗号分隔值
- 是comma-separated values的缩写
- 是纯文本形式存储数字和文本
- 文件由任意数目的记录组成
- 每行记录由多个字段组成
以下例子:
Linux从入门到高级,linux,¥5000
web自动化测试进阶,python,¥3000
app自动化测试进阶,python,¥6000
Docker容器化技术,linux,¥5000
测试平台开发与实战,python,¥8000
-
读取数据
- 内置函数:
open()
- 内置模块:
csv //有相关读取csv文件的方法
方法:csv.reader(iterable)
- 参数:iterable ,文件或列表对象
- 返回:迭代器,每次迭代会返回一行数据。
- 内置函数:
工程目录结构
# 工程目录结构
.
├── data
│ └── params.csv
├── func
│ ├── __init__.py
│ └── operation.py
└── testcase
├── __init__.py
└── test_add.py
- data 目录:存放 csv 数据文件
- func 目录:存放被测函数文件
- testcase 目录:存放测试用例文件
import pytest
import csv
from openpyxl_study.read_csv.func.operation import my_add
def get_csv():
with open("../data/demo.csv") as f:
#读取这个文件
raw = csv.reader(f)
data = []
for line in raw:
data.append(line)
print(data)
return data
class TestWithCsv:
@pytest.mark.parametrize('x,y,expected',get_csv())
def test_add(self,x,y,expected):
assert my_add(int(x),int(y)) == int(expected)
pytest结合数据驱动json
- json是js对象
- 全称是javascript object notation
- 是一种轻量级的数据交换格式
- json结构
- 对象{'key':value}
- 数组[value1,value2....]
- 例子
-
{ "name:": "hogwarts ", "detail": { "course": "python", "city": "北京" }, "remark": [1000, 666, 888] }
- 查看 json 文件
- pycharm
- txt 记事本
- 读取 json 文件
- 内置函数 open()
- 内置库 json
- 方法:
json.loads() //返回值是dict 字典格式的
- 方法:
json.dumps() //返回值是字符串类型的格式
import json
def get_json():
with open("demo.json") as file:
data = json.load(file)
print(data,type(data))
s = json.dumps(data,ensure_ascii=False)
print(s,type(s))
if __name__ == "__main__":
get_json()
运行结果是:
{'name:': 'hogwarts ', 'detail': {'course': 'python', 'city': '北京'}, 'remark': [1000, 666, 888]} <class 'dict'>
{"name:": "hogwarts ", "detail": {"course": "python", "city": "北京"}, "remark": [1000, 666, 888]} <class 'str'>
{
"case1": [1, 1, 2],
"case2": [3, 6, 9],
"case3": [100, 200, 300]
}
import json
from json_study.func.operation import my_add
import pytest
def get_json():
with open("../data/params.json",'r') as file:
data = json.loads(file.read())
print(data)
return list(data.values())
class TestWithJSON:
@pytest.mark.parametrize('x,y,expected', get_json())
def test_add(self, x, y, expected):
assert my_add(int(x), int(y)) == int(expected)
4. 使用values()方法取值
Python字典提供了values()方法,该方法可以将字典的所有值以列表(list)的形式返回。我们可以通过遍历列表来取出对应的值。具体可以通过以下方式实现:
```
person = {"name": "张三", "age": 18, "gender": "男"}
for value in person.values():
print(value)
```
在上述代码中,我们使用for循环遍历了字典person中的所有值,然后使用value变量取出对应的值。输出结果为:
```
张三
18
男
```
需要注意的是,values()方法返回的值是无序的,因此输出结果的顺序与字典中各个键值对的顺序无关。
Fixture用法
Python测试框架pytest(04)fixture - 测试用例调用fixture、fixture传递测试数据_pytest用例之间值传递fixture_wangmcn的博客-CSDN博客
Fixture特点及优势
- 命令灵活:对于setup,teardown,可以不起这两个名字
- 数据共享:在conftest.py配置里写方法可以实现数据共享,不需要import导入,可以跨文件共享
- scope的层次及神奇的yield组合相当于各种setup和teardown
- 实现参数化
fixture在自动化中的应用
- 场景
- 测试用例执行时,有时用例需要登陆才能执行,有些不需要登陆。setup和teardown无法满足,fixture可以。默认scope(范围)function
- 步骤
- 导入pytest
- 在登陆的函数上面加@pytest.fixture()
- 在要使用的测试方法中传入(登陆函数名称)
- 不穿入的就不登陆直接执行测试方法
-
import pytest def login(): print("登陆操作") def test_search(): print("搜索") def test_cart(): login() print("购物车") def test_order(): login() print("购物车")
import pytest @pytest.fixture() def login(): print("登陆操作") def test_search(): print("搜索") def test_cart(login): print("购物车") def test_order(login): print("购物车")
yield的用法
- 场景:
- 你已经可以将测试方法【当前要执行的或依赖的】解决了,测试方法后销毁清楚数据的要如何进行呢?
- 解决:
- 通过在fixture函数中加入yield关键字,yield时调用第一次返回结果,第二次执行他下面的语句返回
- 步骤:
- 在@pytest.fixture(scope=module)
- 在登陆的方法中加yield,之后加销毁清除的步骤
import pytest
@pytest.fixture()
def login():
# setup操作
print("登陆操作")
token = '123456'
username = 'hogwarts'
yield token, username#相当于return
#teardown操作
print('完成登出操作')
def test_search(login):
token,username = login
print(f'token:{token},username:{username}')
print("搜索")
fixture在自动化中的应用-数据共享
- 场景:
- 你与其他测试工程师合作一起开发时,公共的模块要在不同文件中,要在大家都访问到的地方
- 解决:
- 使用conftest.py这个文件进行数据共享,并且它可以放在不同位置起着不同范围的共享作用
- 前提:
- conftest文件名是不能换的
- 放在项目下是全局的数据共享的地方
- 执行:
- 系统执行到参数login时先从本模块中查找是否有这个名字的变量
- 之后在conftest.py中找是否有
- 步骤:
- 将登陆模块带@pytest.fixture写在conftest.py
conftest.py
import pytest
@pytest.fixture()
def login():
# setup操作
print("登陆操作")
token = '123456'
username = 'hogwarts'
yield token, username#相当于return
#teardown操作
print('完成登出操作')
test_fixturedemo1.py
def test_search(login):
token,username = login
print(f'token:{token},username:{username}')
print("搜索")
def test_cart(login):
print("购物车")
def test_order(login):
print("购物车")
fixture在自动化中的应用-自动应用
- 场景:
- 不想原测试方法有任何改动,或全部都自动实现自动应用,没特例,也都不需要返回值时可以选择自动应用
- 解决:
- 使用fixture中参数autouse=True实现
- 步骤:
- 在放上面加@pytest.fixture(autouse = True)
conftest.py
import pytest
@pytest.fixture(scope="function",autouse=True)
def login():
# setup操作
print("登陆操作")
token = '123456'
username = 'hogwarts'
yield token, username#相当于return
#teardown操作
print('完成登出操作')
test_fixturedemo1.py
def test_search(login):
token,username = login
print(f'token:{token},username:{username}')
print("搜索")
def test_cart():
print("购物车")
def test_order():
print("购物车")
在test方法中没有调用login参数的测试用例,也会默认加了login,在执行每个测试用例之前,都会执行一遍登入登出操作
fixture在自动化中的应用-参数化
- 场景:
- 测试离不开数据,为了数据灵活,一般数据都是通过参数传的
- 解法
- fixture通过固定参数request传递
- 步骤
- 在fixture中增加@pytest.fixture(params=[1,2,3,'linda'])
- 在方法参数写request,方法体里面使用request.param接收参数
import pytest
@pytest.fixture(params=["selenium","appium"])
def login(request):
print(f"用户名:{request.param}")
return request.param
def test_demo1(login):
print(f"demo1 case:数据为:{login}")
运行结果是:
/Users/cangqiongqiyuyunxi/PycharmProjects/pythonProject/venv/bin/python /Applications/PyCharm CE.app/Contents/plugins/python-ce/helpers/pycharm/_jb_pytest_runner.py --target test_fixture_param.py::test_demo1
Testing started at 07:48 ...
Launching pytest with arguments test_fixture_param.py::test_demo1 --no-header --no-summary -q in /Users/cangqiongqiyuyunxi/PycharmProjects/pythonProject/test_fixture
============================= test session starts ==============================
collecting ... collected 2 items
test_fixture_param.py::test_demo1[selenium] 用户名:selenium
PASSED [ 50%]demo1 case:数据为:selenium
test_fixture_param.py::test_demo1[appium] 用户名:appium
PASSED [100%]demo1 case:数据为:appium
============================== 2 passed in 0.01s ===============================
Process finished with exit code 0
使用 yield可以让每条测试用例执行yield后的语句,return的话将不会执行return后的语句
import pytest
@pytest.fixture(params=["selenium","appium"])
def login(request):
print(f"用户名:{request.param}")
yield request.param
print("完成登出操作")
def test_demo1(login):
print(f"demo1 case:数据为:{login}")
运行结果是:
/Users/cangqiongqiyuyunxi/PycharmProjects/pythonProject/venv/bin/python /Applications/PyCharm CE.app/Contents/plugins/python-ce/helpers/pycharm/_jb_pytest_runner.py --path /Users/cangqiongqiyuyunxi/PycharmProjects/pythonProject/test_fixture/test_fixture_param.py
Testing started at 07:49 ...
Launching pytest with arguments /Users/cangqiongqiyuyunxi/PycharmProjects/pythonProject/test_fixture/test_fixture_param.py --no-header --no-summary -q in /Users/cangqiongqiyuyunxi/PycharmProjects/pythonProject/test_fixture
============================= test session starts ==============================
collecting ... collected 2 items
test_fixture_param.py::test_demo1[selenium] 用户名:selenium
PASSED [ 50%]demo1 case:数据为:selenium
完成登出操作
test_fixture_param.py::test_demo1[appium] 用户名:appium
PASSED [100%]demo1 case:数据为:appium
完成登出操作
============================== 2 passed in 0.01s ===============================
Process finished with exit code 0
pytest.ini是什么
- pytest.ini是pytest的配置文件
- 可以修改pytest的默认行为
- 不能使用任何中文符号,包括汉字、空格、引号、冒号等等
pytest.ini
- 修改用例的命名规则
- 配置日志格式,比代码配置更方便
- 添加标签,防止运行过程报警告错误
- 制定执行目录
- 排除搜索目录
pytest配置-改变运行规则
- pytest.ini //是用来配置哪些命名方式的函数、类或者python文件可以运行
- check_demo.py //以check_开头命名的测试文件
- test_first.py //以test_开头命名的测试文件
pytest.ini
[pytest]
;执行check_开头和 test_开头的所有的文件,后面一定要加*
python_files = check_* test_*
;执行所有的以Test和Check开头的类
python_classes = Test* Check*
;执行所有以test_和check_开头的方法
python_functions= test_* check_*
check_demo.py //check开头的类及函数也是需要执行
class CheckDemo:
def check_demo1(self):
pass
def check_demo2(self):
pass
test_first.py //test开头的文件名/函数名/类名也是需要执行
import logging
def inc(x):
return x + 1
def test_answer():
logging.info("这是 answer 测试用例")
logging.info("断言 assert inc(3) == 5 ")
assert inc(3) == 5
class TestDemo:
def test_demo1(self):
logging.info("这是 demo1 测试用例")
pass
def test_demo2(self):
logging.info("这是 demo2 测试用例")
pass
在pytest.ini中添加
addopts = -v -s --alluredir=./results
在终端中键入 pytest -vs可以将所有的测试结果打印出来
(venv) cangqiongqiyuyunxi@cangqiongqiyuyunxideMacBook-Air testini % pytest -vs
==================================================== test session starts =====================================================
platform darwin -- Python 3.9.6, pytest-7.4.0, pluggy-1.3.0 -- /Users/cangqiongqiyuyunxi/PycharmProjects/pythonProject/venv/bin/python
cachedir: .pytest_cache
rootdir: /Users/cangqiongqiyuyunxi/PycharmProjects/pythonProject/testini
configfile: pytest.ini
collected 3 items
test_first.py::test_answer FAILED
test_first.py::TestDemo::test_demo1 PASSED
test_first.py::TestDemo::test_demo2 PASSED
========================================================== FAILURES ==========================================================
________________________________________________________ test_answer _________________________________________________________
def test_answer():
logging.info("这是 answer 测试用例")
logging.info("断言 assert inc(3) == 5 ")
> assert inc(3) == 5
E assert 4 == 5
E + where 4 = inc(3)
test_first.py:12: AssertionError
================================================== short test summary info ===================================================
FAILED test_first.py::test_answer - assert 4 == 5
================================================ 1 failed, 2 passed in 0.04s =================================================
pytest配置-指定/忽略执行目录
;设置执行的路径
;testpaths = bilibili baidu
;忽略某些文件夹/目录
norecursedirs = result logs datas test_demo*
pytest logging收集日志
[pytest]
;日志开关 true false
log_cli = true
;日志级别
log_cli_level = info
;打印详细日志,相当于命令行加 -vs
addopts = --capture=no
;日志格式
log_cli_format = %(asctime)s [%(levelname)s] %(message)s (%(filename)s:%(lineno)s)
;日志时间格式
log_cli_date_format = %Y-%m-%d %H:%M:%S
;日志文件位置
log_file = ./log/test.log
;日志文件等级
log_file_level = info
;日志文件格式
log_file_format = %(asctime)s [%(levelname)s] %(message)s (%(filename)s:%(lineno)s)
;日志文件日期格式
log_file_date_format = %Y-%m-%d %H:%M:%S
log_file = ./log/test.log会将logging的输出存放在test.log中
pytest插件开发[进阶]
- pytest插件分类
- 外部插件:pip install 安装的插件
- 本地插件:pytest自动模块发现机制(conftest.py存放的)
- 内置插件:代码内部的_pytest目录加载
- pytest hook介绍
- pytest hook执行顺序
- pytest常用插件
-
pip install pytest-ordering 控制用例的执行顺序(重点) pip install pytest-xdist 分布式并发执行测试用例(重点) pip install pytest-dependency 控制用例的依赖关系 (了解) pip install pytest-rerunfailures 失败重跑(了解) pip install pytest-assume 多重较验(了解) pip install pytest-random-order 用例随机执行(了解) pip install pytest-html 测试报告(了解)
可以通过PyPI · The Python Package Index
-
搜索pytest看相关的插件
-
pytest执行顺序控制
- 场景:
- 对于集成测试,经常会有上下文依赖关系的测试用例
- 对于10个步骤,拆成10条case,这时候能知道到底执行到哪步报错
- 用例默认执行顺序:自上而下执行
- 解决:
- 可以通过setup,teardown和fixture来解决,也可以使用对应的插件
- 安装:pip install pytest-ordering
- 用法:@pytest.mark.run(order = 2)
- 注意:多个插件装饰器(>2)的时候,有可能会发生冲突
- 场景:
-
pytest并行与分布式执行
场景 1:
测试用例 1000 条,一个用例执行 1 分钟,一个测试人员执行需要 1000 分钟。
通常我们会用人力成本换取时间成本,加几个人一起执行,时间就会 缩短。
如果 10 人一起执行只需要 100 分钟,这就是一种分布式场景。
场景 2:
假设有个报名系统,对报名总数统计,数据同时进行修改操作的时候有可能出现问题,
需要模拟这个场景,需要多用户并发请求数据。
解决:
使用分布式并发执行测试用例。分布式插件:pytest-xdist
安装及运行: pip install pytest-xdist
注意: 用例多的时候效果明显,多进程并发执行,同时支持 allure
from time import sleep
import pytest
def test_foo():
sleep(1)
assert True
def test_bar():
sleep(1)
assert True
def test_bar1():
sleep(1)
assert True
def test_foo1():
sleep(1)
assert True
def test_bar2():
sleep(1)
assert True
def test_bar3():
sleep(1)
assert True
在终端中执行pytest -n auto -vs将会分布式执行我们的测试用例
如果串型执行我们的测试用例 总共会用6s的执行时间
如果分布式执行我们的测试用例 我们将大大缩短执行我们测试用例的时间
pytest hook介绍
hook是一个勾子函数
- hook是个函数,在系统消息触发时被系统调用
- 自动触发机制
- hook函数的名称是确定的
- pytest有非常多的勾子函数
- 使用时直接编写函数体
root
└── pytest_cmdline_main
├── pytest_plugin_registered
├── pytest_configure
│ └── pytest_plugin_registered
├── pytest_sessionstart
│ ├── pytest_plugin_registered
│ └── pytest_report_header
├── pytest_collection #在收集测试用例时就需要hook
│ ├── pytest_collectstart
│ ├── pytest_make_collect_report
│ │ ├── pytest_collect_file
│ │ │ └── pytest_pycollect_makemodule
│ │ └── pytest_pycollect_makeitem
│ │ └── pytest_generate_tests
│ │ └── pytest_make_parametrize_id
│ ├── pytest_collectreport
│ ├── pytest_itemcollected
│ ├── pytest_collection_modifyitems
│ └── pytest_collection_finish
│ └── pytest_report_collectionfinish
├── pytest_runtestloop
│ └── pytest_runtest_protocol
│ ├── pytest_runtest_logstart
│ ├── pytest_runtest_setup
│ │ └── pytest_fixture_setup
│ ├── pytest_runtest_makereport
│ ├── pytest_runtest_logreport
│ │ └── pytest_report_teststatus
│ ├── pytest_runtest_call #将会在调用测试用例时实现
│ │ └── pytest_pyfunc_call
│ ├── pytest_runtest_teardown
│ │ └── pytest_fixture_post_finalizer
│ └── pytest_runtest_logfinish
├── pytest_sessionfinish
│ └── pytest_terminal_summary
└── pytest_unconfigure
原始链接
pytest官方关于hook的说明
https://docs.pytest.org/en/stable/reference.html?#hooks 134
https://docs.pytest.org/en/stable/_modules/_pytest/hookspec.html#pytest_cmdline_parse 76
流程说明原始链接
https://github.com/pytest-dev/pytest/issues/3261 81
找到hook的源码:python3.9/site-packages/_pytest/hookspec.py
conftest.py //将setup和teardown引入在测试用例前后进行调用
from typing import Optional
def pytest_runtest_setup(item: "Item") -> None:
print("setup")
def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None:
print("teardown")
test_hook.py // 实际跑的测试用例
def test_demo1():
print("test hook")
hook函数总结
- hook函数名字固定
- hook函数会被自动执行
- 执行时有先后顺序的
- pytest定义了很多hook函数,可以在不同阶段实现不同的功能