Pytest+requests进行接口自动化测试5.0(5种assert断言的封装 + pymysql)
5 种常用断言模式
- 一、assert封装目标:
- 目录结构
- 二、5 种常见 assert 断言封装
- 1. 字符串包含断言( if str1 in str2)
- 2. 结果相等断言(if dict1 == dict2)
- 3. 结果不相等断言(if dict1 != dict2)
- 4. 断言接口返回值里面的任意一个值( if str1 in str2 )
- 5. 数据库断言
- 1)PyMySQL
- 2) 前置准备(整体架构)
- 3)数据库断言代码实现
- 终:assert_result:统一断言入口
一、assert封装目标:
- 构建一个高可维护、易扩展、企业级的接口自动化测试框架,具备以下能力:
- 实现 5 种常用断言模式
- 支持从 YAML 配置驱动
- 日志清晰、异常可追溯
- Allure 报告正确显示失败/成功
- 工业级健壮性(防 jsonpath 返回 False 等陷阱)
核心理念:让断言失败直接抛出 AssertionError,不手动管理 flag
目录结构
interface_automation/
├── common/ # 公共模块
│ ├── assertion.py # 断言封装(本文核心)
│ ├── recordlog.py # 日志工具
│ └── connection.py # 数据库操作
│ └── operationConfig.py # config.yaml操作文件
├── conf/ # 配置文件
│ ├── setting.py # 日志配置
│ └── config.yaml # 环境、数据库配置
├── testcase/ # 测试用例文件
│ ├── login.yaml # 登录测试用例
二、5 种常见 assert 断言封装
在 common/assertion.py 中实现以下 5 种断言模式
YAML 数据驱动文件示例:
- baseInfo:api_name: 根据id查找叶片数据url: /api/aqc/leaf/getByIdmethod: GETheader:Authorization: "{{get_data(token)}}"test_case:- case_name: 正确查询该id风机叶片的数据params:id: "{{get_data(id)}}"validation:- contains: {code: "200"}- contains: {message: "操作成功"}- contains: {success: true}extract_list:batchNo: $.data.batchNo
外部调用三件套:
from common.assertion import Assertions # 导入工具validation = to.pop('validation') # 获取 validationAssertions().assert_result(validation,response,status_code) # 调用该函数
1. 字符串包含断言( if str1 in str2)
- 功能说明:断言预期结果的字符串是否包含在接口的实际返回结果当中(模糊匹配),常用于:
- 消息提示(举例:message 包含“成功”)
- Token 是否以 Bearer 开头
- 错误信息校验
- 代码实现:
common/assertion.py 中 字符串包含断言封装代码:
import jsonpath
from common.recordlog import logs
import allureclass Assertions:# 1.字符串包含断言def contains_assert(self,value_dict,response,status_code):""":param value_dict: 预期结果: yaml文件当中validation关键字下的结果:param response: 接口返回的 JSON 数据:param status_code: HTTP 状态码"""# 断言状态标识:0代表成功,其他代表失败flag = 0for assert_key,assert_value in value_dict.items():if assert_key == 'code':if str(assert_value) != str(status_code):flag = flag + 1allure.attach(f"预期结果:{assert_value}\n,实际结果:{status_code},响应代码断言结果:失败",allure.attachment_type.TEXT)logs.error(f'validation断言失败:接口返回码{status_code}不等于{assert_value}')else:resp_list = jsonpath.jsonpath(response,f'$..{assert_key}')# 安全判断:jsonpath 找不到时返回 Falseif not resp_list:logs.error(f"字段 '{assert_key}' 在响应中未找到")raise AssertionError(f"断言失败:字段 '{assert_key}' 不存在于响应中")# 安全使用 resp_list[0]if isinstance(resp_list[0],str):resp_list = ''.join(resp_list)else:resp_list = str(resp_list[0])# 执行包含断言if str(assert_value) in str(resp_list):logs.info(f'字符串包含断言成功:预期结果:{assert_value}\n,实际结果:{resp_list}')else:flag = flag + 1allure.attach(f"预期结果:{assert_value}\n,实际结果:{resp_list},响应文本断言结果:失败",allure.attachment_type.TEXT)logs.error(f"响应文本断言结果:失败。预期结果:{assert_value}\n,实际结果:{resp_list}")return flag
- 代码解析
关键点 | 说明 |
---|---|
jsonpath.jsonpath(…) | 用来从复杂的 JSON 数据中找某个字段(任意层级,比如找 message 的值) |
“计数器”:flag | flag = 0:全部通过;flag = 1, 2, 3…:有失败的 |
for | 循环获取到的yaml的数据validation |
检查 非code 的其他字段, 如果找不到 resp_list (resp_list 是空的)报错,直接停止测试
else:resp_list = jsonpath.jsonpath(response,f'$..{assert_key}')# 安全判断:jsonpath 找不到时返回 Falseif not resp_list:logs.error(f"字段 '{assert_key}' 在响应中未找到")raise AssertionError(f"断言失败:字段 '{assert_key}' 不存在于响应中")
如果找到了则将之全部处理为字符串,方便后续使用 in
# 安全使用 resp_list[0]if isinstance(resp_list[0],str):resp_list = ''.join(resp_list)else:resp_list = str(resp_list[0])
关键代码!!包含 断言
if str(assert_value) in str(resp_list):logs.info(f'字符串包含断言成功:预期结果:{assert_value}\n,实际结果:{resp_list}')else:flag = flag + 1allure.attach(f"预期结果:{assert_value}\n,实际结果:{resp_list},响应文本断言结果:失败",allure.attachment_type.TEXT)logs.error(f"响应文本断言结果:失败。预期结果:{assert_value}\n,实际结果:{resp_list}")
2. 结果相等断言(if dict1 == dict2)
- 断言接口的实际返回结果是否与预期结果完全一致(精确匹配),常用于:
- 状态码校验(举例:{“code”: 200} 必须严格等于实际返回的 code)
- 登录成功后返回的标准信息比对(如:{“msg”: “登录成功”})
- 接口数据结构固定字段的精确验证(如分页信息 {“page”: 1, “size”: 10})
- 代码实现:
common/assertion.py 中 结果相等断言封装代码:
def equal_assert(self,value,response):"""相等断言模式:param value: 预期结果,也就是yaml文件里面的validation关键字下的参数,必须为dict类型:param response: 接口的实际返回结果:return: flag标识,0表示测试通过,非0表示测试未通过"""if not isinstance(value, dict) or not isinstance(response, dict):raise TypeError("预期结果和响应必须为字典")# 提取 response 中 value 对应的字段filtered = {k: response[k] for k in value if k in response}if filtered == value:logs.info(f"相等断言成功: {filtered}")return 0else:error_msg = f"相等断言失败: 期望={value}, 实际={filtered}"logs.error(error_msg)allure.attach(error_msg, "相等断言失败", allure.attachment_type.TEXT)raise AssertionError(error_msg)
- 代码解析
检查两个参数是不是都是字典格式
if not isinstance(value, dict) or not isinstance(response, dict):raise TypeError("预期结果和响应必须为字典")
从实际返回的数据 response 中,只取出你在 value 里提到的那些字段
filtered = {k: response[k] for k in value if k in response}
如果过滤后的结果和期望的一样,就打印日志:“成功”,并返回 0 表示通过
if filtered == value:logs.info(f"相等断言成功: {filtered}")return 0
3. 结果不相等断言(if dict1 != dict2)
- 断言接口的实际返回结果是否与预期结果不一致(反向匹配),常用于:
- Token 刷新前后对比(新旧 token 不能相等)
- 验证码更新后内容变化校验(刷新后的验证码应不同于之前)
- 防重复提交场景(两次请求的唯一标识或时间戳不应相同)
- 代码实现:
common/assertion.py 中 结果相等断言封装代码:
def equal_assert(self,value,response):"""不相等断言模式:param value: 预期结果,也就是yaml文件里面的validation关键字下的参数,必须为dict类型:param response: 接口的实际返回结果:return: flag标识,0表示测试通过,非0表示测试未通过"""if not isinstance(value, dict) or not isinstance(response, dict):raise TypeError("必须是字典类型")filtered = {k: response[k] for k in value if k in response}if filtered != value:logs.info(f"不相等断言成功: {filtered} ≠ {value}")return 0else:error_msg = f"不相等断言失败: {filtered} == {value} (不应相等)"logs.error(error_msg)allure.attach(error_msg, "不相等断言失败", allure.attachment_type.TEXT)raise AssertionError(error_msg)
- 代码解析
检查两个参数是不是都是字典格式
if not isinstance(value, dict) or not isinstance(response, dict):raise TypeError("预期结果和响应必须为字典")
从实际返回的数据 response 中,只取出你在 value 里提到的那些字段
filtered = {k: response[k] for k in value if k in response}
如果“实际” ≠ “期望”,就打印日志:“成功”,并返回 0 表示通过
if filtered != value:logs.info(f"不相等断言成功: {filtered} ≠ {value}")return 0
4. 断言接口返回值里面的任意一个值( if str1 in str2 )
- 在整个响应体中搜索是否包含某些关键词,适用于:
- 全文搜索用户信息(如:响应中包含 “张三”)
- 校验 Token 或用户名出现在任意位置
- 错误信息中是否包含关键字(如 “参数错误”)
- 代码实现:
common/assertion.py 中 结果相等断言封装代码:
def any_value_assert(self, expected_list, response):"""断言响应中任意位置包含预期值(全文模糊匹配):param expected_list: ['admin', '超级管理员', 200]:param response: 响应 JSON"""# 提取所有值all_values = jsonpath.jsonpath(response, "$..*")if not all_values or not isinstance(all_values, list):raise AssertionError("响应为空或格式错误")for expected in expected_list:found = Falsefor val in all_values:if str(expected) in str(val):found = Truelogs.info(f"找到匹配值: '{expected}' in '{val}'")breakif not found:logs.error(f"未找到值: '{expected}'")raise AssertionError(f"值 '{expected}' 未在响应中找到")
- 代码解析(和 字符串包含断言写法类似 )
关键点 | 说明 |
---|---|
jsonpath.jsonpath(…) | 用来从复杂的 JSON 数据中找某个字段(任意层级,比如找 message 的值) |
for | 循环获取到的yaml的数据validation |
5. 数据库断言
1)PyMySQL
pymysql 是 Python 连接 MySQL 的“桥梁”
- 自动化测试中的用途
- 验证数据库字段是否更新(如订单状态)
- 清理测试数据(DELETE FROM temp_data)
- 准备测试数据(INSERT 测试用户)
- 校验接口是否正确写入数据库
安装:
pip install pymysql
功能 | 方法 |
---|---|
连接数据库 | pymysql.connect() |
创建游标(必须创建) | .cursor() |
执行 SQL | .cursor.execute(sql) / .cursor.executemany(sql, seq_of_params) |
获取查询结果 | .cursor.fetchone() /.cursor.fetchall() |
提交事务(常见) | .cursor.commit() |
回滚事务(常见) | .cursor.rollback() |
防止注入 | 使用 %s 占位符 |
返回字典 | cursorclass=DictCursor |
验证过程中若遇到报错:需下载cryptography库
报错:
RuntimeError: 'cryptography' package is required for sha256_password or caching_sha2_password auth methods
安装:
pip install cryptography
问题原因:
MySQL 服务器使用了 caching_sha2_password 作为用户认证方式(MySQL 8.0+ 的默认认证插件),而 PyMySQL 在这种模式下需要一个额外的加密库:cryptography
2) 前置准备(整体架构)
config.yaml 数据库配置↓
operationConfig.py → 读取配置↓
connection.py → 连接数据库 + 执行SQL↓
assertion.py → 封装所有断言(包含db断言)↓
测试用例中调用 Assertions().assert_result(...) → 自动完成所有验证
-
- config.yaml:数据库等配置驱动
# 数据库配置
mysql:host: 192.192.2.6port: 3306user: fdafdapassword: "fdakj@2023.."database: ai_quality_controlcharset: utf8mb4
-
- login.yaml:测试用例中的预期结果
validation:- contains: {code: "200"}- contains: {message: "成功"}- eq: {'msg': '登录成功'}- ne: {'msg': '登录失败'}- db: 'select * from w_leaf where id = 1'
-
- operationConfig.py:通用配置读取工具(可直接获取到数据库配置)
import yaml
from conf.setting import FILE_PATHclass OperationYaml:"""封装读取 YAML 配置文件"""def __init__(self, file_path=None):if file_path is None:self.__file_path = FILE_PATH['conf']else:self.__file_path = file_pathself.__data = None # 缓存数据,避免重复读文件self.__load_data()def __load_data(self):"""私有方法:加载 YAML 文件"""try:with open(self.__file_path, 'r', encoding='utf-8') as f:self.__data = yaml.safe_load(f) # 安全解析 YAMLexcept Exception as e:print(f"读取 YAML 文件失败:{e}")self.__data = {}def get(self, *keys):"""通用获取方法,支持多层嵌套:param keys: 键的路径,如 get('api_envi', 'host'):return: 对应值"""if not self.__data:return Nonedata = self.__datafor key in keys:if isinstance(data, dict) and key in data:data = data[key]else:print(f"找不到路径: {keys}")return Nonereturn datadef get_envi(self, option):"""快捷方法:获取接口环境地址"""return self.get('api_envi', option)def get_mysql_conf(self, key):"""快捷方法:获取数据库配置"""return self.get('mysql', key)
-
- connection.py:数据库操作封装文件
from common.recordlog import logs
from conf.operationConfig import OperationYaml
import pymysqlclass ConnectMysql:def __init__(self):self.connection = OperationYaml()mysql_conf = {'host': self.connection.get_mysql_conf('host'),'port': self.connection.get_mysql_conf('port'),'user': self.connection.get_mysql_conf('user'),'password': self.connection.get_mysql_conf('password'),'database': self.connection.get_mysql_conf('database'),'charset': self.connection.get_mysql_conf('charset') or 'utf8mb4' # 防空}try:self.conn = pymysql.connect(**mysql_conf)# cursor=pymysql.cursors.DictCursor:将数据库表字段显示:以key-value形式显示self.cursor = self.conn.cursor(cursor=pymysql.cursors.DictCursor)logs.info("""成功链接到MySql数据库host:{host}port:{port}database:{database}""".format(**mysql_conf))except Exception as e:logs.error(e)def close(self):if self.conn and self.cursor:self.cursor.close()self.conn.close()def query(self, sql):"""查询数据"""try:self.cursor.execute(sql)self.conn.commit()res = self.cursor.fetchall()return resexcept Exception as e:logs.error(e)finally:self.close()def insert(self, sql):"""新增"""passdef update(self, sql):"""修改"""passdef delete(self, sql):"""删除"""pass
- 代码解释
使用 operationConfig.py 中封装好的解析方法读取 config.yaml 数据库配置
from conf.operationConfig import OperationYamlself.connection = OperationYaml()mysql_conf = {'host': self.connection.get_mysql_conf('host'),'port': self.connection.get_mysql_conf('port'),'user': self.connection.get_mysql_conf('user'),'password': self.connection.get_mysql_conf('password'),'database': self.connection.get_mysql_conf('database'),'charset': self.connection.get_mysql_conf('charset') or 'utf8mb4' # 防空}
pymysql.connect 连接数据库, pymysql.cursors.DictCursor (以字典形式清晰展示返回内容)
self.conn = pymysql.connect(**mysql_conf)# cursor=pymysql.cursors.DictCursor:将数据库表字段显示:以key-value形式显示self.cursor = self.conn.cursor(cursor=pymysql.cursors.DictCursor)logs.info("""成功链接到MySql数据库host:{host}port:{port}database:{database}""".format(**mysql_conf))
自动关闭数据库连接
def close(self):if self.conn and self.cursor:self.cursor.close()self.conn.close()
封装数据库查询方法 (.cursor( ) 是操作数据库的“操作手柄”)
def query(self, sql):"""查询数据"""try:self.cursor.execute(sql)self.conn.commit()res = self.cursor.fetchall()return resexcept Exception as e:logs.error(e)finally:self.close()
创建游标 | .cursor() |
---|---|
执行 SQL | .cursor.execute(sql) / .cursor.executemany(sql, seq_of_params) |
提交事务(常见) | .cursor.commit() |
获取查询结果 | .cursor.fetchone() /.cursor.fetchall() |
3)数据库断言代码实现
from common.connection import ConnectMysqldef assert_mysql(self,expected_sql):"""数据库断言:param expected_sql:预期结果,也就是yaml文件的SQL语句:return: 返回flag标识,0标识测试通过,非0表示测试失败"""flag = 0conn = ConnectMysql()db_value = conn.query(expected_sql)if db_value is not None:logs.info(f"数据库断言成功")else:flag = flag + 1logs.error("数据库断言失败,请检查数据库是否存在该数据!")return flag
终:assert_result:统一断言入口
assertion.py 文件
def assert_result(self,expected,response,status_code):"""断言模式,通过all_flag标记:param expected: 预期结果:param response: 接口实际返回结果,需要json格式:param status_code: 接口实际返回状态码:return:"""all_flag = 0# 断言状态标识:0 代表成功,其他代表失败try:for yq in expected:for key,value in yq.items():if key == 'contains':flag = self.contains_assert(value,response,status_code)all_flag = all_flag + flagelif key == 'eq':flag = self.equal_assert(value,response)all_flag = all_flag + flagelif key == 'ne':flag = self.not_equal_assert(value,response)all_flag = all_flag + flagelif key == 'db':flag = self.assert_mysql(value)all_flag = all_flag + flagassert all_flag == 0logs.info('测试成功')except Exception as e:logs.error(f'测试失败,异常信息:{e}')raise
→ 遍历每一项
→ 判断 key 是哪种断言
→ 调用对应方法
→ 返回 flag(0 或 1)
→ 累加到 all_flag
→ 最后 assert all_flag == 0 → 测试通过
- 外部使用方法
from common.assertion import Assertions # 导入工具validation = to.pop('validation') # 获取 validationAssertions().assert_result(validation,response,status_code) # 调用该函数