知识点复习-可乔

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 文件

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
    image

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 安装

  1. 安装 Java,需要配置环境变量。
  2. 安装 Allure ,需要配置环境变量。
  3. 安装插件
  • 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-severitiespytest --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:直接通过注解或调用方法添加。
  • 语法: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() 处理

下拉框/日期控件定位

  • 场景:
    • <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)
1 个赞