接口自动化测试 - pytest 结合 YML/Json Schema/logging/allure
目录
1. YML
1.1 YML 介绍
yml / yaml 不是一种编程语言, 而是一种语法格式(一种数据序列化语言), 类似于 json/xml, 当相比于 json, yml 有着更加简洁/干净的语法, 它不使用 花括号/方括号/逗号, 而是使用缩进和换行来表示层级关系.
可以使用 .yaml 或 .yml 作为一个 YML/YAML 文件的扩展名, 两者没有区别, 可以互换使用.
yml 的特点如下:
-
YAML 是⼀种⾮常简单的基于⽂本的⼈类可读的语⾔, ⽤于在⼈和计算机之间交换数据
-
YAML 不是一种编程语言, 它主要⽤于存储配置信息
-
YAML 和 Python 一样, 使用缩进来组织数据
-
YAML 还减少了 JSON 和 XML ⽂件中的⼤部分 '噪⾳' 格式, 例如引号、⽅括号和⼤括号
- YAML 区分⼤⼩写
- YAML 不允许使⽤制表符 Tab 键, (你之所按下 Tab YAML 仍能使⽤,是因为编辑器被配置为按下 Tab 键会导致插⼊适当数量的空格)
- YAML 语法遵循严格缩进
1.2 PyYAML
Python 提供了 PyYaml 库, 可以让我们以 '读' 或者 '写' 的方式实现 Python 数据结构(主要是字典/列表)与 YAML 格式数据之间的双向转换.
安装 pyYaml:
pip install PyYAML==6.0.1
1.2.1 YML 与 Python 数据类型映射
| 数据类型 | YAML 示例 | 转换后的 Python 数据类型 | Python 代码示例 |
| 简单标量 | key: value | str (字符串) | {'key': 'value'} |
| 整数 | int_key: 123 | int (整数) | {'int_key': 123} |
| 浮点数 | float_key: 123.456 | float (浮点数) | {'float_key': 123.456} |
| 布尔值 | bool_key: true | bool (布尔值) | {'bool_key': True} |
| 字符串 | string_key: "This is a string" | str (字符串) | {'string_key': 'This is a string'} |
| 列表 | list_key: - item1 - item2 - item3 |
list (列表) | {'list_key': ['item1', 'item2', 'item3']} |
| 映射 (字典) | map_key: sub_key1: sub_value1 sub_key2: sub_value2 |
dict (字典) | 'map_key': { 'sub_key1': 'sub_value1', 'sub_key2': 'sub_value2' } |
| 嵌套结构 | nested_key: list_key: - item1 - item2 map_key: sub_key1: sub_value1 sub_key2: sub_value2 |
dict (包含 list 和 dict 的字典) | { 'nested_key': { 'list_key': ['item1', 'item2'], 'map_key': { 'sub_key1': 'sub_value1', 'sub_key2': 'sub_value2' } } } |
在 yml 中表示字符串时, 值的部分, 带不带引号都可以:

Json 转 yml 的工具:
1.2.2 读写 yml 文件
在 Python 中, 使用 safe_dump() 来向 yml 文件中写数据, 使用 safe_load() 来从 yml 文件中读数据.
-
yaml.safe_dump(): 写入. 将 Python 对象(如字典、列表)序列化成 YAML 格式的字符串或写入到一个文件中
-
yaml.safe_load(): 读取. 解析 YAML 格式的字符串或文件, 并将其转换成 Python 对象.
注意:
- 使用 open 打开文件时, 基准路径为 当前项目所在路径.
1.2.2.1 yaml.safe_dump()


# 向 yml 文件中写入内容.
def write_yml(data):
# 以文件上下文管理器形式打开文件, 避免文件资源泄露.
# 'w': 普通写入, 会清除原来的内容
# 'a': 追加写, 不会清除原来的内容
with open("./test/data1.yml", mode="w", encoding="utf-8") as f:
yaml.safe_dump(data,
stream=f,
# 将 allow_unicode 设置为 True, 以便正确处理中文.
allow_unicode=True)
def test_write_yml():
data = {
"code": "SUCCESS",
"errMsg": "",
"data": [
{
"id": 19150,
"title": "我是张三", # <--- 包含中文
"content": "##在这里写下一篇博客bb", # <--- 包含中文
"userId": 1,
"deleteFlag": 0,
"createTime": "2025-02-28 15:06",
"updateTime": "2025-02-28T07:06:22.000+00:0",
"loginUser": False
}
]
}
write_yml(data)
在向 yml 文件写入内容前, 可以先将 yml 文件中的内容清空:
# 清空文件内容 def clear_yml(): with open("./test/data.yml", mode="w", encoding="utf-8") as f: f.truncate()
1.2.2.2 yml.safe_load()

# 读取 yml 文件.
def read_yml():
with open("./test/data.yml", 'r', encoding="utf-8") as f:
data = yaml.safe_load(stream=f)
return data
def test_read_yml():
data = read_yml()
print(data)
注意:
yml 文件中展示列表时, 表示列表中的元素时, 应该换行写, 并且带有 - 和缩进 :
# 正确的格式
score:
- 80
- 90
- 99
但是, IDE 展示的是这样的(没有带缩进):
# 错误的格式
score:
- 80
- 90
- 99
这可能是由于 IDE / pyYaml 版本导致的.
但是, 我们要清楚, 带缩进的才是正确的 yml 格式.
2. Json Schema
2.1 Json Schema 介绍 + 安装
Json Schema 是用来定义和校验 Json 的 Web 规范, 简单来说, Json Schema 就是用来检验接口返回值(响应 body 中的 Json), 是否符合预期.
因此, Json Schema 是我们从接口文档中, 根据预期返回的 Json 去设计的, 然后, 再使用验证器, 将实际返回的 Json 和设计好的 Json Schema 进行比较, 校验实际返回的 Json 是否符合预期.
校验接口返回值时, 我们注重的是 Json 的完整性校验:
-
需要返回的字段, 是否都存在.
-
返回字段值的类型, 是否正确 (值是可变的, 但是类型是固定的).
我们不会关注每个字段具体的值, 第一是因为, 响应返回的值每次可能都不一样, 我们不能在 Json Schema 中将某个字段的值写死; 第二是因为响应中的值是非常多的, 如果每个字段的值都校验, 根本校验不完....
当然, 在某些特定场景下, 也会对值进行校验, 比如: 需要对值的格式或范围进行校验:
-
字符串格式: 使用正则表达式(pattern)校验字符串是否符合特定格式(如电子邮件、日期时间、手机号等)
-
数值范围: 使用 minimum, maximum 关键字校验数字是否在某个区间内
-
枚举值: 使用 enum 关键字, 确保字段的值必须是预定义列表中的某一个(例如: status 字段的值必须是 ["success", "failed", "pending"] 中的一个).
-
数组属性: 校验数组的长度 (最小长度, 最大长度), 以及数组中的元素是否唯一.
安装 Json Schema:
pip install jsonschema==4.23.0
2.2 Json Schema 数据类型
通过 Json Schema 的 type 字段, 规定 Json 字段的数据类型, 就可以验证 Json 中每个字段的数据类型是否符合预期.
| type | 解释 |
| string | 字符串类型, 用于文本数据. |
| number | 数字类型, 表示整数和浮点数. |
| integer | 整数类型, 只能表示整数. |
| boolean | 布尔类型, 值为 true 或 false. |
| object | 对象类型, 表示 JSON 对象. |
| array | 数组类型, 用于列表或集合. |
| null | 空值类型(表示 None/null). |
2.3 Json Schema 简单使用
首先, 我们需要先定义好 Json Schema, 再将实际的 Json 和 Json Schema 进行校验.

定义 Json Schema 时, 需要关注以下几个关键字段:
- type: 表示数据类型
- required: 表示 Json 中必须存在的字段
- properties: 表示 Json 中字段的属性
- instance: 表示待校验的 Json 数据.
- schema: 表示校验规则, 也就是 Json Schema.
如上图所示, 实际返回的 Json 中的 data 字段是 boolean 类型的, 而 Json Schema 中规定 data 应该是 string 类型的, 所以实际返回的 Json 不符合预期, 因此测试不通过:

若将 Json Schema 中 data 类型修改为 boolean, 则测试通过:

综上, 校验 Json 需要定义好 Json Schema, 但是当 Json 多时, 定义 Json Schema 也就非常麻烦了, 因此, 我们可以使用工具根据 Json 自动生成 Json Schema:
https://tooltt.com/json2schema/
https://tooltt.com/json2schema/
但是, 工具不是万能的, 通过工具生成的 Json Schema 可能存在错误, 比如:
这个接口是 Java 写的接口, 其中一个字段的类型为布尔类型(true/false), 但是 Python 中的布尔类型为 True/False, 因此功能就可能把这个字段识别为一个 string 类型.
因此通过工具生成 Json Schema 后, 需要我们再自行的检查一下.
通过 requests 获取响应中的 Json, 再根据 Json Schema 去判断, 也是一样的:

要根据 Json 字段的数据类型, 去定义 Json Schema 中的 type:

2.4 最大值 / 最小值
Json Schema 中, 通过以下两组字段, 均可以设定最大值和最小值:
-
minimum 和 maximum: 指定数值的最大值和最小值(包含等于, 左闭右闭)
-
exclusiveMinimum 和 exclusiveMaximum: 指定数值必须严格 大于 或者 小于 某个值(不包含等于, 左开右开)
2.5 字符串特殊校验
在 Json Schema 中, 可以使用以下字段, 对字符串类型的数据进行校验:
- pattern: 使⽤正则表达式来验证字符串是否符合特定的模式.
- minimum 和 maximum: 规定字符串长度的最小值和最大值
常见的正则表达式:
https://www.runoob.com/regexp/regexp-syntax.html
| \s | 匹配任何空白字符, 包括空格、制表符、换页符等. | ![]() |
| \S | 匹配任何非空白字符. | ![]() |
| {n} | n 是一个非负整数。匹配确定的 n 次。例如,o{2} 不能匹配 "Bob" 中的 o,但是能匹配 "food" 中的两个 o。 | |
| {n,} | n 是一个非负整数。至少匹配n 次。例如,o{2,} 不能匹配 "Bob" 中的 o,但能匹配 "foooood" 中的所有 o。o{1,} 等价于 o+。o{0,} 则等价于 o*。 | |
| {n,m} |
m 和 n 均为非负整数,其中 n <= m。最少匹配 n 次且最多匹配 m 次。例如,o{1,3} 将匹配 "fooooood" 中的前三个 o。o{0,1} 等价于 o?。请注意在逗号和两个数之间不能有空格。 |
|
| \d |
匹配任意一个阿拉伯数字(0 到 9)。等价于 [0-9] |
|
2.6 数组约束
在 Json Schema 中, 使用以下字段, 对 列表/集合/数组 类型的数据进行校验:
- minItems 和 maxItems: 指定数组的最小和最大长度.
-
uniqueItems: 指定数组中的元素是否是唯一的. (True 则表示每个元素必须是唯一的)
-
items: 定义数组中每个元素的类型和约束.
2.7 对象约束
在 Json Schema 中, 使用以下字段, 对 Json 对象进行约束:
-
minProperties 和 maxProperties: 指定对象属性个数的最小值和最大值.
-
additionalProperties: 是否允许对象中存在除 properties 中定义的其他属性(默认为 True, 允许存在其他属性)
在 Json Schema 中使用 aminProperties 和 maxProperties / dditionalProperties 时, 要注意这些关键字定义的位置:
- 它们只对自己所在的、"type": "object" 的那一层级生效
- 对于该对象内部嵌套的任何子对象, 或者外部的父对象, 这些规则都是无效的.
通过 minProperties 和 maxProperties 指定属性个数的范围:
如果属性的个数大于 maxProperties, 那么测试失败:

通过 additionalProperties 指定是否可以返回未被 properties 定义的属性.
additionalProperties 默认为 True, 允许返回其他属性, 若手动将 additionalProperties 设置为 False, 则不能返回没有在 properties 中定义 的属性:

2.8 必须返回的字段
上文的 additionalProperties 属性, 只能检查有没有返回多余的字段, 而不能检查应有的字段有没有返回.
也就是说, 即使返回的 Json 对象中没有任何内容, validate 也会检查通过:

那我们就可以通过 Json Schema 的 required 属性来规定哪些属性是必须返回的, 如果返回的 Json 没有包含这些属性, 那么 validate 就检验失败:

当 required 中规定的字段都被返回时, validate 才会校验成功:

2.9 依赖关系
在一些特定情况下, 返回的数据之间是有依赖关系的, 比如: 返回学生信息时, 如果返回了学生的姓名信息, 那也一定要返回该学生的年龄/性别等信息.
因此, 属性之间是会存在依赖关系的(如果某个属性存在, 则必须存在另一个属性).
在 Json Schema 中, 通过 dependentRequired 定义属性之间的依赖关系:

上述定义的依赖关系就是:
- 若 name 字段返回, 则 age 和 gender 字段也必须返回.
- 若 name 字段没有返回, 则 age 和 gender 字段不做要求.
因此, name 返回后, 若 age 或 gender 没有返回, 则 validate 检验失败:

注意, 定义 dependentRequired 时也要注意定义的位置, 它也只对自己所在的、"type": "object" 的那一层级生效.
3. logging 日志模块
logging 是 Python 标准库中的模块, 无需通过 pip 安装.
logging 允许将不同级别的日志信息输出到 控制台/文件/网络 等多个目标中.
使用 logging 有以下三种方式
- 使用全局 logging
- 使用自定义 logger 输出到控制台
- 使用自定义 logger 输出到文件
使用 logging/logger 输出日志时, 可以自定义日志级别:
- debug
- info
- warning
- error
-
critical
默认的日志级别是 info, 只会输出 info 及其以上级别的日志信息.
3.1 全局 logging
通过 logging.basicConfig 来设置日志级别, 设置后的日志级别是全局的, 会影响后续自定义获取 Logger 对象.

import logging
# 设置 logging 的日志级别为 Debug.
# 输出 DEBUG 及以上级别的日志信息.
logging.basicConfig(level=logging.DEBUG)
logging.debug("这是一条 Debug 级别的日志信息.")
logging.info("这是一条 Info 级别的日志信息.")
logging.warning("这是一条 Warning 级别的日志信息.")
logging.error("这是一条 Error 级别的日志信息.")
logging.critical("这是一条 Critical 级别的日志信息.")
3.2 自定义 Logger 对象
3.2.1 输出到控制台
- 通过 logging.getLogger 获取 Logger 对象通过传参设置模块名称, 用于区分不同模块的日志信息(全局 logging 模块名称为 root)
- 通过 setLevel 设置日志级别(DEBUG < INFO < WARNING < ERROR < CRITICAL)

import logging
# 获取 logger 对象, 自定义 logger 名称为 'my_logger'
logger = logging.getLogger('my_logger')
# 设置 logger 的日志级别为 WARNING(输出 WARNING 及以上级别的日志信息)
logger.setLevel(logging.WARNING)
logger.debug("这是一条 debug 日志.")
logger.info("这是一条 info 日志.")
logger.warning("这是一条 warning 日志.")
logger.error("这是一条 error 日志.")
logger.critical("这是一条 critical 日志.")
3.2.2 输出到文件
实际工作中, 自动化脚本都是在服务器中运行的, 我们不会去关注控制台的日志信息, 而是将日志输出到文件中, 查看日志文件.
- 通过 logging.FileHandler 创建一个文件处理器, 传入日志文件的路径.
- 通过 Logger.addHandler 将处理器添加到 Logger 对象中.

3.2.3 设置日志格式
从上文可以看到, 输出的日志是不易于观察的.
因此, 我们可以创建日志格式对象, 设置日志的格式:
- 通过 Logger = logging.getLogger 获取 Logger 对象
- 通过 Logger.setLevel(DEBUG/INFO/...) 设置日志级别
- 通过 FileHandler = logging.FileHandler(filename) 获取文件处理器, 指定日志文件路径
- 通过 Formatter = logging.Formatter(fmt) 获取日志格式对象, 设置日志格式
- 通过 FileHandler.setFormatter(Formatter) 将刚才获取的 日志格式 对象添加到 文件处理器中
- 通过 Logger.addHandler(FileHandler) 将文件处理器添加到 Logger 对象中
注意: 设置 日志格式 时, 一定要在获取 handler 后, 添加 handler 前进行.

上述代码仅仅添加了文件处理器, 日志信息只会被输出到指定的文件中, 是不会输出到控制台上的, 因此也可以在创建文件处理器的同时, 也可以通过 logging.StreamHandler() 创建一个控制台处理器, 并将控制台处理器 添加到 Logger 对象中, 方便在控制太观察日志信息:

import logging
# 1. 获取 Logger 对象, 自定义 Logger 名称为 'my_logger'
logger = logging.getLogger('my_logger')
# 2. 设置 logger 的日志级别为 WARNING(输出 WARNING 及以上级别的日志信息)
logger.setLevel(logging.WARNING)
# 3. 创建文件处理器. 将日志信息, 输出到 my_logger.log 文件中.
loggerHandler = logging.FileHandler("my_logger.log")
# 4. 创建控制台处理器, 将日志信息输出到控制台.
loggerStreamHandler = logging.StreamHandler()
# 5. 创建一个日志格式对象.
formatter = logging.Formatter("%(asctime)s %(levelname)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d)] - %(message)s")
# 6. 将日志格式对象, 添加到 文件处理器 和 控制台处理器中.
loggerHandler.setFormatter(formatter)
loggerStreamHandler.setFormatter(formatter)
# 7. 将 文件处理器 和 控制台处理器, 添加到 Logger 对象中.
logger.addHandler(loggerHandler)
logger.addHandler(loggerStreamHandler)
# 8. 输出日志信息.
logger.debug("这是一条 debug 日志.")
logger.info("这是一条 info 日志.")
logger.warning("这是一条 warning 日志.")
logger.error("这是一条 error 日志.")
logger.critical("这是一条 critical 日志.")
4. allure
官方文档: https://allurereport.org/docs/pytest-configuration/
4.1 安装
Mac 用户, 安装过程非常简单:
-
安装 Allure 命令行工具:用来生成最终的 HTML 报告
-
在终端(Terminal, 应用)通过 Homebrew 安装: brew install allure
-
(如果没有 Homebrew, 要先安装)
-
-
安装 allure-pytest 插件:用来在运行测试时收集数据
-
在项目虚拟环境的终端安装: pip install allure-pytest==2.13.5
-
Windows 用户:
-
下载压缩包: https://github.com/allure-framework/allure2/releases/download/2.30.0/allure- 2.30.0.zip
-
解压, 并将其 bin 目录添加到系统环境变量
- 项目虚拟环境的终端安装: pip install allure-pytest==2.13.5
验证是否安装成功:
- 在终端(Mac)/cmd(Win)输入 allure --version 若出现版本号, 则安装成功

- 在项目虚拟环境的终端输入 allure --version 若出现版本号, 则安装成功

以上都出现版本号时, 才算安装成功.
如果有人在 终端/cmd 上能出现版本号, 但是 pycharm/IDE 上不出现, 这很可能是 IDE 配置导致的, 可以修改以下配置, 修改完重启:

4.2 使用
使用 allure 生成测试报告, 需要以下两个步骤:
- 生成测试结果
- 查看测试报告
4.2.1 生成测试结果
# 将测试结果, 保存到 allure-results 文件夹中.
# (以项目路径为基准路径)
pytest --alluredir=allure-results
# 将测试结果, 保存到 allure-results 文件夹中, 并清除上次的测试结果.
# (以项目路径为基准路径)
pytest --alluredir=allure-results --clean-alluredir

综上, 生成测试结果时, 需要输入这么一大段命令, 是很麻烦的, 因此, 我们可以在 pytest.ini 文件中对 addopts 进行配置, 生成测试结果时直接输入 pytest 即可:
[pytest]
# 在 pytest 后默认跟上 -vs 参数, 显示详细的测试结果.
# --alluredir=allure-results --clean-alluredir 参数, 用于生成测试报告.
addopts = -vs --alluredir=allure-results --clean-alluredir
可以发现, 生成的测试结果是 .json 文件, 不具备可读性, 因此需要查看测试报告.
4.2.2 查看测试报告
查看测试报告有两种方式:
- 启动⼀个本地服务器来在浏览器中展示测试报告
- 从测试结果⽣成一个测试报告的 HTML
第一种方式: 启动⼀个本地服务器来在浏览器中展示测试报告, 命令:
# <allure-results> 是生成的测试结果的路径
allure serve [options] <allure-results>
其中, options 可有可无, 可以指定 IP / 端口(如果不指定的话, 端口是随机的):
- --host :指定服务器监听的主机地址, 默认为 localhost
- --port :指定服务器监听的端⼝号, 默认为 0(⾃动选择空闲端⼝)
这种方式是启动了一个本地服务器去展示测试报告的, 因此, 当服务器终止时, 测试报告就无法查看了:

第二种方式, 是直接根据测试结果⽣成了一个测试报告的 HTML, 我们随时都可以用浏览器打开这个 HTML 查看测试结果:
# <allure-results>: 测试结果的路径(.json 文件的路径)
# <reports>: 要生成的测试报告的路径
# --clean: 若 <reports> 路径下存在同名的 HTML 文件, 则进行清除.
allure generate [options] <allure-results> -o <reports> --clean



这样, 就不需要服务器去查看测试报告了, 我们随时都可以通过 HTML 文件去查看.
注意:
- 一定要先生成测试结果(.json), 再去生成测试报告(.html), 因为测试报告是基于测试结果生成的.
- 如果测试内容改变, 要先更新测试结果, 再生成测试报告.
END
更多推荐


所有评论(0)