Pytest
Pytest 命名规则
类型 | 规则 |
---|---|
文件 | test_开头 或者 _test 结尾 |
类 | Test 开头 |
方法/函数 | test_开头 |
注意:测试类中不可以添加__init__构造函数 |
pytest 用例结构
- 三部分构成
- 用例名称
- 用例步骤
- 用例断言
类级别的用例示例
class TestXXX:
def setup(self):
# 资源准备
pass
def teardown(self):
# 资源销毁
pass
def test_XXX(self):
# 测试步骤1
# 测试步骤2
# 断言 实际结果 对比 预期结果
assert ActualResult == ExpectedResult
Pytest 测试框架结构(setup/teardown)
测试装置介绍
类型 | 规则 |
---|---|
setup_module/teardown_module | 全局模块级 |
setup_class/teardown_class | 类级,只在类中前后运行一次 |
setup_function/teardown_function | 函数级,在类外 |
setup_method/teardown_method | 方法级,类中的每个方法执行前后 |
setup/teardown | 在类中,运行在调用方法的前后(重点) |
pytest 参数化用例
参数化
- 通过参数的方式传递数据,从而实现数据和脚本分离。
- 并且可以实现用例的重复生成与执行。
参数化实现方案
- pytest 参数化实现方法
- 装饰器:
@pytest.mark.parametrize
@pytest.mark.parametrize("username,password",[["right","right"], ["wrong","wrong"]])
def test_param(username,password):
login(username,password)
Mark:参数化测试函数使用
参数化:单参数情况
- 单参数,可以将数据放在列表中
search_list = ['appium','selenium','pytest']
@pytest.mark.parametrize('name',search_list)
def test_search(name):
assert name in search_list
参数化:多参数情况
- 将数据放在列表嵌套元组中
- 将数据放在列表嵌套列表中
import pytest
# 数据放在元组中
@pytest.mark.parametrize("test_input,expected",[
("3+5",8),("2+5",7),("7+5",12)
])
def test_mark_more(test_input,expected):
assert eval(test_input) == expected
# 数据放在列表中
@pytest.mark.parametrize("test_input,expected",[
["3+5",8],["2+5",7],["7+5",12]
])
def test_mark_more(test_input,expected):
assert eval(test_input) == expected
参数化:用例重命名-添加 ids 参数
- 通过ids参数,将别名放在列表中
import pytest
@pytest.mark.parametrize("test_input,expected",[
("3+5",8),("2+5",7),("7+5",12)
],ids=['add_3+5=8','add_2+5=7','add_3+5=12'])
def test_mark_more(test_input,expected):
assert eval(test_input) == expected
参数化:用例重命名-添加 ids 参数(中文)
# 创建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")
@pytest.mark.parametrize("test_input,expected",[
("3+5",8),("2+5",7),("7+5",12)
],ids=["3和5相加","2和5相加","7和5相加"])
def test_mark_more(test_input,expected):
assert eval(test_input) == expected
参数化:笛卡尔积
两组数据
a=[1,2,3]
b=[a,b,c]
对应有几种组合形势 ?9
(1,a),(1,b),(1,c)
(2,a),(2,b),(2,c)
(3,a),(3,b),(3,c)
import pytest
@pytest.mark.parametrize("b",["a","b","c"])
@pytest.mark.parametrize("a",[1,2,3])
#如果有多个装饰器,由近到远,先a后b,一共有9条案例
def test_param1(a,b):
print(f"笛卡积形式的参数化中 a={a} , b={b}")
使用 Mark 标记测试用例
场景:只执行符合要求的某一部分用例 可以把一个web项目划分多个模块,然后指定模块名称执行。
解决: 在测试用例方法上加 @pytest.mark.标签名
执行: -m 执行自定义标记的相关用例
pytest -s test_mark_zi_09.py -m=webtest
pytest -s test_mark_zi_09.py -m apptest
pytest -s test_mark_zi_09.py -m "not ios"
pytest 设置跳过、预期失败
Mark:跳过(Skip)及预期失败(xFail)
- 这是 pytest 的内置标签,可以处理一些特殊的测试用例,不能成功的测试用例
- skip - 始终跳过该测试用例
- skipif - 遇到特定情况跳过该测试用例
- xfail - 遇到特定情况,产生一个“期望失败”输出
@pytest.skip(reason="这个有Bug")
def test_add_3(self):
#a的参数超出范围
expect = Calculator().add(-100,99)
assert expect == "参数大小超出范围"
xfail 使用场景
- 与 skip 类似 ,预期结果为 fail ,标记用例为 fail
- 用法:添加装饰器
@pytest.mark.xfail
pytest 运行用例
运行多条用例
- 运行 某个/多个 用例包
- 运行 某个/多个 用例模块
- 运行 某个/多个 用例类
- 运行 某个/多个 用例方法
运行多条用例方式
- 执行包下所有的用例:
pytest/py.test [包名]
- 执行单独一个 pytest 模块:
pytest 文件名.py
- 运行某个模块里面某个类:
pytest 文件名.py::类名
- 运行某个模块里面某个类里面的方法:
pytest 文件名.py::类名::方法名
运行结果分析
- 常用的:fail/error/pass
- 特殊的结果:warning/deselect
pytest 测试用例调度与运行
命令行参数-使用缓存状态
-
--lf(--last-failed)
只重新运行故障。 -
--ff(--failed-first)
先运行故障然后再运行其余的测试
pytest常用命令行参数
命令行参数 - 常用命令行参数
—help
-x 用例一旦失败(fail/error),就立刻停止执行
–maxfail=num 用例达到
-m 标记用例
-k 执行包含某个关键字的测试用例
-v 打印详细日志
-s 打印输出日志(一般-vs一块儿使用)
—collect-only(测试平台,pytest 自动导入功能 )
python 执行 pytest
- 使用 main 函数
- 使用 python -m pytest 调用 pytest(jenkins 持续集成用到)
Python 代码执行 pytest - main 函数
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 结合数据驱动 YAML
数据驱动
- 什么是数据驱动?
- 数据驱动就是数据的改变从而驱动自动化测试的执行,最终引起测试结果的改变。简单来说,就是参数化的应用。数据量小的测试用例可以使用代码的参数化来实现数据驱动,数据量大的情况下建议大家使用一种结构化的文件(例如 yaml,json 等)来对数据进行存储,然后在测试用例中读取这些数据。
- 应用:
- App、Web、接口自动化测试
- 测试步骤的数据驱动
- 测试数据的数据驱动
- 配置的数据驱动
yaml 文件介绍
- 对象:键值对的集合,用冒号 “:” 表示
- 数组:一组按次序排列的值,前加 “-”
- 纯量:单个的、不可再分的值
- 字符串
- 布尔值
- 整数
- 浮点数
- Null
- 时间
- 日期
# 编程语言
languages:
- 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文件内容 - 方法:
yaml.safe_dump(f)
- 安装:
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 目录:存放测试用例文件
# 工程目录结构
├── data
│ └── data.yaml
├── func
│ ├── __init__.py
│ └── operation.py
└── testcase
├── __init__.py
└── test_add.py
测试准备
- 被测对象:
operation.py
- 测试用例:
test_add.py
- 测试数据:
data.yaml
# operation.py 文件内容
def my_add(x, y):
result = x + y
return result
# test_add.py 文件内容
class TestWithYAML:
@pytest.mark.parametrize('x,y,expected', [[1, 1, 2]])
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
Pytest 数据驱动结合 yaml 文件
# 读取yaml文件
def get_yaml():
"""
获取json数据
:return: 返回数据的结构:[[1, 1, 2], [3, 6, 9], [100, 200, 300]]
"""
with open('../datas/data.yaml', 'r') as f:
data = yaml.safe_load(f)
return data
Pytest 结合数据驱动 Excel
读取 Excel 文件
- 第三方库
xlrd
xlwings
pandas
- openpyxl
openpyxl 库的安装
- 安装:
pip install openpyxl
- 导入:
import openpyxl
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
工程目录结构
- data 目录:存放 excel 数据文件
- func 目录:存放被测函数文件
- testcase 目录:存放测试用例文件
# 工程目录结构
.
├── data
│ └── params.excel
├── func
│ ├── __init__.py
│ └── operation.py
└── testcase
├── __init__.py
└── test_add.py
测试准备
- 被测对象:
operation.py
- 测试用例:
test_add.py
# operation.py 文件内容
def my_add(x, y):
result = x + y
return result
# test_add.py 文件内容
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)
测试准备
- 测试数据:
params.xlsx
Pytest 数据驱动结合 Excel 文件
# 读取Excel文件
import openpyxl
import pytest
def get_excel():
# 获取工作簿
book = openpyxl.load_workbook('../data/params.xlsx')
# 获取活动行(非空白的)
sheet = book.active
# 提取数据,格式:[[1, 2, 3], [3, 6, 9], [100, 200, 300]]
values = []
for row in sheet:
line = []
for cell in row:
line.append(cell.value)
values.append(line)
return values
Pytest 结合数据驱动 json
json 文件介绍
- json 是 JS 对象
- 全称是 JavaScript Object Notation
- 是一种轻量级的数据交换格式
- json 结构
- 对象
{"key": value}
- 数组
[value1, value2 ...]
- 对象
{
"name:": "hogwarts ",
"detail": {
"course": "python",
"city": "北京"
},
"remark": [1000, 666, 888]
}
json 文件使用
- 查看 json 文件
- pycharm
- txt 记事本
- 读取 json 文件
- 内置函数 open()
- 内置库 json
- 方法:
json.loads()
将json转为字典 - 方法:
json.dumps()
# 读取json文件内容
def get_json():
with open('demo.json', 'r') as f:
data = json.loads(f.read())
print(data)
工程目录结构
- data 目录:存放 json 数据文件
- func 目录:存放被测函数文件
- testcase 目录:存放测试用例文件
# 工程目录结构
.
├── data
│ └── params.json
├── func
│ ├── __init__.py
│ └── operation.py
└── testcase
├── __init__.py
└── test_add.py
测试准备
- 被测对象:
operation.py
- 测试用例:
test_add.py
- 测试数据:
params.json
import pytest
# operation.py 文件内容
def my_add(x, y):
result = x + y
return result
# test_add.py 文件内容
class TestWithJSON:
@pytest.mark.parametrize('x,y,expected', [[1, 1, 2]])
def test_add(self, x, y, expected):
assert my_add(int(x), int(y)) == int(expected)
# params.json 文件内容
{
"case1": [1, 1, 2],
"case2": [3, 6, 9],
"case3": [100, 200, 300]
}
Pytest 数据驱动结合 json 文件
# 读取json文件
import json
def get_json():
"""
获取json数据
:return: 返回数据的结构:[[1, 1, 2], [3, 6, 9], [100, 200, 300]]
"""
with open('demo.json', 'r') as f:
data = json.loads(f.read())
#打印结果:{'case1': [1, 1, 2], 'case2': [3, 6, 9], 'case3': [100, 200, 300]}
print(data)
#打印结果为:dict_values([[1, 1, 2], [3, 6, 9], [100, 200, 300]])
print(data.values())
#返回结果为:[[1, 1, 2], [3, 6, 9], [100, 200, 300]]
return list(data.values())
Fixture 用法
Fixture 特点及优势
- 1、命令灵活:对于 setup,teardown,可以不起这两个名字
- 2、数据共享:在 conftest.py 配置⾥写⽅法可以实现数据共享,不需要 import 导⼊。可以跨⽂件共享
- 3、scope 的层次及神奇的 yield 组合相当于各种 setup 和 teardown
- 4、实现参数化
Fixture 在自动化中的应用- 基本用法
- 场景:
测试⽤例执⾏时,有的⽤例需要登陆才能执⾏,有些⽤例不需要登陆。
setup 和 teardown ⽆法满⾜。fixture 可以。默认 scope(范围)function
-
步骤:
- 1.导⼊ pytest
- 2.在登陆的函数上⾯加@pytest.fixture()
- 3.在要使⽤的测试⽅法中传⼊(登陆函数名称),就先登陆
- 4.不传⼊的就不登陆直接执⾏测试⽅法。
Fixture 用法
Fixture 在自动化中的应用 - 作用域
取值 | 范围 | 说明 |
---|---|---|
function | 函数级 | 每一个函数或方法都会调用 |
class | 类级别 | 每个测试类只运行一次 |
module | 模块级 | 每一个.py 文件调用一次 |
package | 包级 | 每一个 python 包只调用一次(暂不支持) |
session | 会话级 | 每次会话只需要运行一次,会话内所有方法及类,模块都共享这个方法 |
Fixture 用法
Fixture 在自动化中的应用 - yield 关键字
- 场景:
你已经可以将测试⽅法【前要执⾏的或依赖的】解决了,
测试⽅法后销毁清除数据的要如何进⾏呢?
- 解决:
通过在 fixture 函数中加⼊ yield 关键字,yield 是调⽤第⼀次返回结果,
第⼆次执⾏它下⾯的语句返回。
- 步骤:
在@pytest.fixture(scope=module)。
在登陆的⽅法中加 yield,之后加销毁清除的步骤
Fixture 在自动化中的应用 - 数据共享
- 场景:
你与其他测试⼯程师合作⼀起开发时,公共的模块要在不同⽂件中,要在⼤家都访问到的地⽅。
- 解决:
使⽤ conftest.py 这个⽂件进⾏数据共享,并且他可以放在不同位置起着不同的范围共享作⽤。
-
前提:
- conftest ⽂件名是不能换的
- 放在项⽬下是全局的数据共享的地⽅
-
执⾏:
- 系统执⾏到参数 login 时先从本模块中查找是否有这个名字的变量什么的,
- 之后在 conftest.py 中找是否有。
- 步骤:
将登陆模块带@pytest.fixture 写在 conftest.py
Fixture 在自动化中的应用 - 自动应用
场景:
不想原测试⽅法有任何改动,或全部都⾃动实现⾃动应⽤,
没特例,也都不需要返回值时可以选择⾃动应⽤
解决:
使⽤ fixture 中参数 autouse=True 实现
步骤:
在⽅法上⾯加 @pytest.fixture(autouse=True)
Fixture 在自动化中的应用 -参数化
场景:
测试离不开数据,为了数据灵活,⼀般数据都是通过参数传的
解决:
fixture 通过固定参数 request 传递
步骤:
在 fixture 中增加@pytest.fixture(params=[1, 2, 3, ‘linda’])
在⽅法参数写 request,方法体里面使用 request.param 接收参数
Fixture 的用法总结
- 模拟 setup,teardown(一个用例可以引用多个 fixture)
- yield 的用法
- 作用域( session,module, 类级别,方法级别 )
- 自动执行 (autouse 参数)
- conftest.py 用法,一般会把 fixture 写在 conftest.py 文件中(这个文件名字是固定的,不能改)
- 实现参数化
pytest.ini 配置
pytest.ini 是什么
- pytest.ini 是 pytest 的配置文件
- 可以修改 pytest 的默认行为
- 不能使用任何中文符号,包括汉字、空格、引号、冒号等等
pytest.ini
- 修改用例的命名规则
- 配置日志格式,比代码配置更方便
- 添加标签,防止运行过程报警告错误
- 指定执行目录
- 排除搜索目录
pytest 配置- 改变运行规则
;执行check_开头和 test_开头的所有的文件,后面一定要加*
python_files = check_* test_*
;执行所有的以Test和Check开头的类
python_classes = Test* Check*
;执行所有以test_和check_开头的方法
python_functions= test_* check_*
pytest 配置- 添加默认参数
addopts = -v -s --alluredir=./results
pytest 配置- 指定/忽略执行目录
;设置执行的路径
;testpaths = bilibili baidu
;忽略某些文件夹/目录
norecursedirs = result logs datas test_demo*
pytest 配置- 日志
配置参考链接:pytest logging 收集日志
总结 pytest.ini
- 修改用例的命名规则
- 配置日志格式,比代码配置更方便
- 指定执行目录
- 排除搜索目录
- 添加标签,防止运行过程报警告错误
- 添加默认参数
pytest 执行顺序控制
场景:
对于集成测试,经常会有上下文依赖关系的测试用例。
比如 10 个步骤,拆成 10 条 case,这时候能知道到底执行到哪步报错。
用例默认执行顺序:自上而下执行
解决:
可以通过 setup,teardown 和 fixture 来解决。也可以使用对应的插件。
安装:pip install pytest-ordering
用法:@pytest.mark.run(order=2)
注意:多个插件装饰器(>2)的时候,有可能会发生冲突
pytest测试用例并行运行与分布式
Pytest 并行与分布式执行
场景 1:
测试用例 1000 条,一个用例执行 1 分钟,一个测试人员执行需要 1000 分钟。
通常我们会用人力成本换取时间成本,加几个人一起执行,时间就会 缩短。
如果 10 人一起执行只需要 100 分钟,这就是一种分布式场景。
场景 2:
假设有个报名系统,对报名总数统计,数据同时进行修改操作的时候有可能出现问题,
需要模拟这个场景,需要多用户并发请求数据。
解决:
使用分布式并发执行测试用例。分布式插件:pytest-xdist
安装及运行: pip install pytest-xdist
注意: 用例多的时候效果明显,多进程并发执行,同时支持 allure
可以看到,最终运行时间只需要6s,我的电脑是真6核,假12核
-n auto:可以自动检测到系统的CPU核数;从测试结果来看,检测到的是逻辑处理器的数量,即假12核
使用auto等于利用了所有CPU来跑用例,此时CPU占用率会特别高
分布式执行测试用例原则
- 用例之间是独立的,不要有依赖关系
- 用例执行没有顺序,随机顺序都能正常执行
- 每个用例都能重复运行,运行结果不会影响其他用例
Allure2
Allure2 介绍
- Allure 是由 Java 语⾔开发的⼀个轻量级,灵活的测试报告⼯具。
- Allure 多平台的 Report 框架。
- Allure ⽀持多语⾔,包括 python、JaveScript、PHP、Ruby 等。
- 可以为开发/测试/管理等人员提供详尽的的测试报告,包括测试类别、测试步骤、日志、图片、视频等。
- 可以为管理层提供高水准的统计报告。
- 可以集成到 Jenkins 生成在线的趋势汇总报告。
Allure2 安装
- 安装 Java,需要配置环境变量。
- 安装 Allure ,需要配置环境变量。
- 安装插件
- Python:
pip install allure-pytest
。 - Java:Maven插件安装。
插件安装-Python
- 安装 Python 插件
allure-pytest
- 执行命令:
pip install allure
# linux/mac
> pip list |grep allure
allure-pytest x.xx.x
# windows
> pip list |findstr allure
allure-pytest x.xx.x
使用 Allure2 运行方式-Python
- 使用
--alluredir
参数生成测试报告。
# 在测试执行期间收集结果
pytest [测试用例/模块/包] --alluredir=./result/ (—alluredir这个选项 用于指定存储测试结果的路径)
# 生成在线的测试报告
allure serve ./result
使用 Allure2 生成精美报告
- 生成测试报告需要使用命令行工具 allure
- 命令格式:
allure [option] [command] [command options]
步骤一:在测试执行期间收集结果
—alluredir这个选项 用于指定存储测试结果的路径
pytest [测试文件] -s –q --alluredir=./result/
如果要清除已经生成的报告的历史记录,可以添加参数–clean-alluredir
pytest [测试文件] -s –q --alluredir=./result/ --clean-alluredir
步骤二:查看测试报告,注意这里的serve书写
allure serve ./result/
Allure 报告生成的两种方式
- 方式一:在线报告,会直接打开默认浏览器展示当前报告。
- 方式二:静态资源文件报告(带 index.html、css、js 等文件),需要将报告布署到 web 服务器上。
方式一:测试完成后查看实际报告,在线查看报告,会直接打开默认浏览器展示当前报告。
allure serve ./result/ (注意这里的serve书写)
Allure 报告生成报告 - 方式二
- 应用场景:如果希望随时打开报告,可以生成一个静态资源文件报告,将这个报告布署到 web 服务器上,启动 web 服务,即可随时随地打开报告。
- 解决方案:使用
allure generate
生成带有 index.html 的结果报告。这种方式需要两个步骤: - 第一步:生成报告。
- 第二步:打开报告。
生成报告
allure generate ./result
打开报告
allure open ./report/
常用参数
-
allure generate
可以指定输出路径,也可以清理上次的报告记录。 - -o / –output 输出报告的路径。
- -c / –clean 如果报告路径重复。
-
allure open
打开报告。 - -h / –host 主机 IP 地址,此主机将用于启动报表的 web 服务器。
- -p / –port 主机端口,此端口将用于启动报表的 web 服务器,默认值:0。
生成报告,指定输出路径,清理报告。
allure generate ./result -o ./report --clean
打开报告,指定IP地址和端口。
allure open -h 127.0.0.1 -p 8883 ./report/
Allure2报告中添加用例标题
Allure 用法
|方法名|方法参数|参数说明|
|—|—|—|
|@allure.epic()|epic 描述|敏捷里面的概念,定义史诗,往下是 feature|
|@allure.feature()|模块名称|功能点的描述,往下是 story|
|@allure.story()|用户故事|用户故事,往下是 title|
|@allure.title(用例的标题)|用例的标题|重命名 html 报告名称|
|@allure.step()|操作步骤|测试用例的步骤|
|@allure.testcase()|测试用例的链接地址|对应功能测试用例系统里面的 case|
|@allure.issue()|缺陷|对应缺陷管理系统里面的链接|
|@allure.description()|用例描述|测试用例的描述|
|@allure.severity()|用例等级|blocker,critical,normal,minor,trivial|
|@allure.link()|链接|定义一个链接,在测试报告展现|
|@allure.attachment()|附件|报告添加附件|
Allure2报告中添加用例分类
- 应用场景:可以为项目,以及项目下的不同模块对用例进行分类管理。也可以运行某个类别下的用例。
- 报告展示:类别会展示在测试报告的 Behaviors 栏目下。
- Allure 提供了三个装饰器:
-
@allure.epic
:敏捷里面的概念,定义史诗,往下是 feature。 -
@allure.feature
:功能点的描述,理解成模块往下是 story。 -
@allure.story
:故事 story 是 feature 的子集。
-
Allure 运行 feature/story
- allure 相关的命令查看 :
pytest --help|findstr allure
- 通过指定命令行参数,运行 epic/feature/story 相关的用例:
pytest 文件名 --allure-epics=EPICS_SET --allure-features=FEATURES_SET --allure-stories=STORIES_SET
# 只运行 epic 名为 "需求1" 的测试用例
pytest --alluredir ./results --clean-alluredir --allure-epics=需求1
# 只运行 feature 名为 "功能模块2" 的测试用例
pytest --alluredir ./results --clean-alluredir --allure-features=功能模块2
# 只运行 story 名为 "子功能1" 的测试用例
pytest --alluredir ./results --clean-alluredir --allure-stories=子功能1
# 运行 story 名为 "子功能1和子功能2" 的测试用例
pytest --alluredir ./results --clean-alluredir --allure-stories=子功能1,子功能2
# 运行 feature + story 的用例(取并集)
pytest --alluredir ./results --clean-alluredir --allure-features=功能模块1 --allure-stories=子功能1,子功能2
Allure epic/feature/story 的关系
epic:敏捷里面的概念,用来定义史诗,相当于定义一个项目。
feature:相当于一个功能模块,相当于 testsuite,可以管理很多个子分支 story。
story:相当于对应这个功能或者模块下的不同场景,分支功能。
epic 与 feature、feature 与 story 类似于父子关系。
epic: 需求 — 登录
feature: 大的功能点 微信登录,gq登录,微博登录
story: 小的功能点 登录成功、登录失败、特殊情况的测试用例
Allure2 报告中添加用例描述
- 应用场景:Allure 支持往测试报告中对测试用例添加非常详细的描述语,用来描述测试用例详情。
- Allure 添加描述的四种方式:
- 方式一:使用装饰器
@allure.description()
传递一个字符串参数来描述测试用例。 - 方式二:使用装饰器
@allure.description_html
传递一段 HTML 文本来描述测试用例。 - 方式三:直接在测试用例方法中通过编写文档注释的方法来添加描述。
- 方式四:用例代码内部动态添加描述信息。
- 方式一:使用装饰器
Allure2报告中添加用例优先级
- 应用场景:用例执行时,希望按照严重级别执行测试用例。
- 解决:可以为每个用例添加一个等级的装饰器,用法:
@allure.severity
。 - Allure 对严重级别的定义分为 5 个级别:
- Blocker级别:中断缺陷(客户端程序无响应,无法执行下一步操作)。
- Critical级别:临界缺陷( 功能点缺失)。
- Normal级别:普通缺陷(数值计算错误)。
- Minor级别:次要缺陷(界面错误与UI需求不符)。
- Trivial级别:轻微缺陷(必输项无提示,或者提示不规范)。
- 使用装饰器添加用例方法/类的级别。
- 类上添加的级别,对类中没有添加级别的方法生效。
- 运行时添加命令行参数
--allure-severities
:pytest --alluredir ./results --clean-alluredir --allure-severities normal,blocker
Allure2 添加用例标签-fixture
- 应用场景:fixture 和 finalizer 是分别在测试开始之前和测试结束之后由 Pytest 调用的实用程序函数。Allure 跟踪每个 fixture 的调用,并详细显示调用了哪些方法以及哪些参数,从而保持了调用的正确顺序。
import pytest
@pytest.fixture()
def func(request):
print("这是一个fixture方法")
# 定义一个终结器,teardown动作放在终结器中
def over():
print("session级别终结器")
request.addfinalizer(over)
class TestClass(object):
def test_with_scoped_finalizers(self,func):
print("测试用例")
#打印结果为:
#这是一个fixture方法
# 测试用例
# session级别终结器
Allure2报告中支持记录失败重试功能
Allure2 失败重试功能应用场景
- Allure 可以收集用例运行期间,重试的用例的结果,以及这段时间重试的历史记录。
Allure2 失败重试功能
- 重试功能可以使用 pytest 相关的插件,例如
pytest-rerunfailures
。 - 重试的结果信息,会展示在详情页面的”Retries” 选项卡中。
import pytest
@pytest.mark.flaky(reruns=2, reruns_delay=2)
def test_rerun2():
assert False
Allure2 报告中添加附件(图片)- Python
- 语法:
allure.attach(body, name=None, attachment_type=None, extension=None):
,参数解释:- body:要写入附件的内容
- name:附件名字。
- attachment_type:附件类型,是
allure.attachment_type
其中的一种(支持 PNG、JPG、BMP、GIF 等)。 - extension:附件的扩展名。
import allure
class TestWithAttach:
def test_pic2(self):
with open("./logo.png",mode="rb") as f :
file = f.read()
allure.attach(file,"页面截图",allure.attachment_type.PNG)
Allure2 报告中添加日志 - Python
- 代码输出到用例详情页面。
- 运行用例:
pytest --alluredir ./results --clean-alluredir
(注意不要加-vs)。
@allure.feature("功能模块2")
class TestWithLogger:
@allure.story("子功能1")
@allure.title("用例1")
def test_case1(self):
logger.info("用例1的 info 级别的日志")
logger.debug("用例1的 debug 级别的日志")
logger.warning("用例1的 warning 级别的日志")
logger.error("用例1的 error 级别的日志")
logger.fatal("用例1的 fatal 级别的日志")
Allure2 报告中添加附件(html)- Python
- 语法:
allure.attach(body, name, attachment_type, extension)
,参数解释:- body:要写入附件的内容(HTML 代码块)。
- name:附件名字。
- attachment_type:附件类型,是
allure.attachment_type
其中的一种。 - extension:附件的扩展名。
import allure
class TestWithAttach:
def test_html(self):
allure.attach('<head></head><body> a page </body>',
'附件是HTML类型',
allure.attachment_type.HTML)
def test_html_part(self):
allure.attach('''html代码块''',
'附件是HTML类型',
allure.attachment_type.HTML)
Allure2 报告中添加附件-视频
Allure2 报告中添加附件(视频)应用场景
- 应用场景:在做 UI 自动化测试时,可以将页面截图,或者出错的页面进行截图,将截图添加到测试报告中展示,辅助定位问题。
- 解决方案:
- Python:使用
allure.attach.file()
添加视频。 - Java:直接通过注解或调用方法添加。
- Python:使用
- 语法:
allure.attach.file(source, name, attachment_type, extension)
,参数解释:- source:文件路径,相当于传一个文件。
- name:附件名字。
- attachment_type:附件类型,是
allure.attachment_type
其中的一种。 - extension:附件的扩展名。
import allure
class TestWithAttach:
def test_video(self):
allure.attach.file("xxx.mp4",
name="视频",
attachment_type=allure.attachment_type.MP4,
extension="mp4")
web自动化测试
什么时候可以做UI自动化测试
- 业务流程不频繁改动
- UI 元素不频繁改动
- 需要频繁回归的场景
- 核心场景等
脚本优化
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
class TestDemo01():
def setup_method(self, method):
# 实例化chromedriver
self.driver = webdriver.Chrome()
# 添加全局隐式等待
self.driver.implicitly_wait(5)
def teardown_method(self, method):
# 关闭driver
self.driver.quit()
def test_demo01(self):
# 访问网站
self.driver.get("https://www.baidu.com/")
# 设置窗口
self.driver.set_window_size(1330, 718)
# 点击输入框
self.driver.find_element(By.ID, "kw").click()
# 输入框输入信息
self.driver.find_element(By.ID, "kw").send_keys("霍格沃兹测试开发")
# 点击搜索按钮
self.driver.find_element(By.ID, "su").click()
# 等待界面加载
time.sleep(5)
# 元素定位后获取文本信息
res = self.driver.find_element(By.XPATH, "//*[@id='1']/h3/a").get_attribute("text")
# 打印文本信息
print(res)
# 添加断言
assert "霍格沃兹测试开发" in res
# 查看界面展示
time.sleep(5)
eb 浏览器控制
浏览器控制
- 模拟功能测试中对浏览器的操作
||操作|使用场景|
| — | — | — |
|get|打开浏览器|web自动化测试第一步| driver.get(‘http://www.ceshiren.com’)
|refresh|浏览器刷新|模拟浏览器刷新| driver.refresh()
|back|浏览器退回|模拟退回步骤| driver.back()
|maximize_window|最大化浏览器|模拟浏览器最大化| driver.maximize_window()
|minimize_window|最小化浏览器|模拟浏览器最小化| driver.minimize_window()
常见控件定位方法
HTML铺垫
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>测试人论坛</title>
</head>
<body>
<a href="https://ceshiren.com/" class="link">链接</a>
</body>
</html>
- 标签:
<a>
- 属性:href
- 类属性: class
Selenium定位方式
Selenium提供了八种定位方式:Locator strategies | Selenium
方式 | 描述 |
---|---|
class name | class 属性对应的值 |
css selector(重点) | css 表达式 |
id(重点) | id 属性对应的值 |
name(重点) | name 属性对应的值 |
link text | 查找其可见文本与搜索值匹配的锚元素 |
partial link text | 查找其可见文本包含搜索值的锚元素。如果多个元素匹配,则只会选择第一个元素。 |
tag name | 标签名称 |
xpath(重点) | xpath表达式 |
强制等待与隐式等待
为什么要添加等待
- 避免页面未渲染完成后操作,导致的报错
总结
类型 | 使用方式 | 原理 | 适用场景 |
---|---|---|---|
直接等待 | time.sleep(等待时间)) | 强制线程等待 | 调试代码,临时性添加 |
隐式等待 | driver.implicitly_wait(等待时间) | 在时间范围内,轮询查找元素 | 解决找不到元素问题,无法解决交互问题 |
显式等待 | WebDriverWait(driver实例, 最长等待时间, 轮询时间).until(结束条件) | 设定特定的等待条件,轮询操作 | 解决特定条件下的等待问题,比如点击等交互性行为 |
常见控件交互方法
点击,输入,清空
# 点击百度搜索框
driver.find_element(By.ID,"kw").click()
# 输入"霍格沃兹测试开发"
driver.find_element(By.ID,"kw").send_keys("霍格沃兹测试开发")
# 清空搜索框中信息
driver.find_element(By.ID,"kw").clear()
获取元素文本
driver.find_element(By.ID, “id”).text
获取这个元素的name属性的值
driver.find_element(By.ID, “id”).get_attribute(“name”)
Web 弹框定位
- 场景
- web 页面 alert 弹框
- 解决:
- web 需要使用
driver.switchTo().alert()
处理
- web 需要使用
下拉框/日期控件定位
- 场景:
-
<input>
标签组合的下拉框无法定位 -
<input>
标签组合的日期控件无法定位
-
- 解决:
- 面对这些元素,我们可以引入 JS 注入技术来解决问题。
文件上传定位
- 场景:
- input 标签文件上传
- 解决:
- input 标签直接使用 send_keys()方法
css基础语法
类型 | 表达式 |
---|---|
标签 | 标签名 |
类 | .class属性值 |
ID | #id属性值 |
属性 | [属性名=‘属性值’] |
//在console中的写法
// https://www.baidu.com/
//标签名
$('input')
//.类属性值
$('.s_ipt')
//#id属性值
$('#kw')
//[属性名='属性值']
$('[name="wd"]')
css关系定位
类型 | 格式 |
---|---|
并集 | 元素,元素 |
邻近兄弟(了解即可) | 元素+元素 |
兄弟(了解即可) | 元素1~元素2 |
父子 | 元素>元素 |
后代 | 元素 元素 |
//在console中的写法
//元素,元素
$('.bg,.s_ipt_wr,.new-pmd,.quickdelete-wrap')
//元素>元素
$('#s_kw_wrap>input')
//元素 元素
$('#form input')
//元素+元素,了解即可
$('.soutu-btn+input')
//元素1~元素2,了解即可
$('.soutu-btn~i')
css 顺序关系
类型 | 格式 |
---|---|
父子关系+顺序 | 元素 元素 |
父子关系+标签类型+顺序 | 元素 元素 |
//:nth-child(n)
$('#form>input:nth-child(2)')
//:nth-of-type(n)
$('#form>input:nth-of-type(1)')
xpath 高级用法
-
[last()]
: 选取最后一个 -
[@属性名='属性值' and @属性名='属性值']
: 与关系 -
[@属性名='属性值' or @属性名='属性值']
: 或关系 -
[text()='文本信息']
: 根据文本信息定位 -
[contains(text(),'文本信息')]
: 根据文本信息包含定位 - 注意:所有的表达式需要和
[]
结合
# 选取最后一个input标签
//input[last()]
# 选取属性name的值为passward并且属性pwd的值为123456的input标签
//input[@name='passward' and @pwd='123456']
# 选取属性name的值为passward或属性pwd的值为123456的input标签
//input[@name='passward' or @pwd='123456']
# 选取所有文本信息为'霍格沃兹测试开发'的元素
//*[text()='霍格沃兹测试开发']
# 选取所有文本信息包'霍格沃兹'的元素
//*[contains(text(),'霍格沃兹')]
常见 expected_conditions
类型 | 示例方法 | 说明 |
---|---|---|
element | element_to_be_clickable() | |
visibility_of_element_located() | 针对于元素,比如判断元素是否可以点击,或者元素是否可见 | |
url | url_contains() | 针对于 url |
title | title_is() | 针对于标题 |
frame | frame_to_be_available_and_switch_to_it(locator) | 针对于 frame |
alert | alert_is_present() | 针对于弹窗 |
鼠标事件
- 双击
- 拖动元素
- 指定位置(悬浮)
鼠标事件-双击
-
double_click(元素对象)
: 双击元素
class TestMouseDemo:
def setup_class(self):
self.driver = webdriver.Chrome()
self.driver.implicitly_wait(3)
def teardown_class(self):
self.driver.quit()
def test_double_click(self):
# 演练环境
self.driver.get("https://vip.ceshiren.com/#/ui_study")
ele = self.driver.find_element(By.ID, "primary_btn")
ActionChains(self.driver).double_click(ele).perform()
time.sleep(2)
鼠标事件-拖动元素
-
drag_and_drop(起始元素对象, 结束元素对象)
: 拖动并放开元素
class TestMouseDemo:
def setup_class(self):
self.driver = webdriver.Chrome()
self.driver.implicitly_wait(3)
def teardown_class(self):
self.driver.quit()
def test_drag_and_drop(self):
# 演练环境
self.driver.get("https://vip.ceshiren.com/#/ui_study/action_chains")
item_left = self.driver.find_element(By.CSS_SELECTOR,'#item1')
item_right = self.driver.find_element(By.CSS_SELECTOR,'#item3')
ActionChains(self.driver).drag_and_drop(item_left, item_right).perform()
time.sleep(5)
鼠标事件-悬浮
-
move_to_element(元素对象)
: 移动到某个元素
class TestMouseDemo:
def setup_class(self):
self.driver = webdriver.Chrome()
self.driver.implicitly_wait(3)
def teardown_class(self):
self.driver.quit()
def test_hover(self):
# 演练环境
self.driver.get("https://vip.ceshiren.com/#/ui_study/action_chains2")
time.sleep(2)
title = self.driver.find_element(By.CSS_SELECTOR, '.title')
ActionChains(self.driver).move_to_element(title).perform()
options = self.driver.find_element(By.CSS_SELECTOR,'.options>div:nth-child(1)')
ActionChains(self.driver).click(options).perform()
time.sleep(5)
滚轮/滚动操作
- 滚动到元素
- 根据坐标滚动
- 注意: selenium 版本需要在 4.2 之后
滚轮/滚动操作-滚动到元素
-
scroll_to_element(WebElement对象)
:滚动到某个元素
class TestScrollDemo:
def setup_class(self):
self.driver = webdriver.Chrome()
self.driver.implicitly_wait(3)
def teardown_class(self):
self.driver.quit()
def test_scoll_to_element(self):
# 演练环境
self.driver.get("https://ceshiren.com/")
# 4.2 之后才提供这个方法
ele = self.driver.find_element\
(By.XPATH, "//*[text()='怎么写高可用集群部署的测试方案?']")
ActionChains(self.driver).scroll_to_element(ele).perform()
time.sleep(5)
滚轮/滚动操作-根据坐标滚动
scroll_by_amount(横坐标, 纵坐标)
class TestScrollDemo:
def setup_class(self):
self.driver = webdriver.Chrome()
self.driver.implicitly_wait(3)
def teardown_class(self):
self.driver.quit()
def test_scroll_to_amount(self):
# 演练环境
self.driver.get("https://ceshiren.com/")
# 4.2 之后才提供这个方法
ActionChains(self.driver).scroll_by_amount(0, 10000).perform()
time.sleep(5)