5 种常用断言模式
- 一、assert封装目标:
- [二、5 种常见 assert 断言封装](#二、5 种常见 assert 断言封装)
-
- [1. 字符串包含断言( if str1 in str2)](#1. 字符串包含断言( if str1 in str2))
- [2. 结果相等断言(if dict1 == dict2)](#2. 结果相等断言(if dict1 == dict2))
- [3. 结果不相等断言(if dict1 != dict2)](#3. 结果不相等断言(if dict1 != dict2))
- [4. 断言接口返回值里面的任意一个值( if str1 in str2 )](#4. 断言接口返回值里面的任意一个值( if str1 in str2 ))
- [5. 数据库断言](#5. 数据库断言)
-
- 1)PyMySQL
- [2) 前置准备(整体架构)](#2) 前置准备(整体架构))
- 3)数据库断言代码实现
- 终:assert_result:统一断言入口
一、assert封装目标:
- 构建一个高可维护、易扩展、企业级的接口自动化测试框架,具备以下能力:
- 实现 5 种常用断言模式
- 支持从 YAML 配置驱动
- 日志清晰、异常可追溯
- Allure 报告正确显示失败/成功
- 工业级健壮性(防 jsonpath 返回 False 等陷阱)
核心理念:让断言失败直接抛出 AssertionError,不手动管理 flag
目录结构
python
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 数据驱动文件示例:
yaml
- baseInfo:
api_name: 根据id查找叶片数据
url: /api/aqc/leaf/getById
method: GET
header:
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
外部调用三件套:
python
from common.assertion import Assertions # 导入工具
validation = to.pop('validation') # 获取 validation
Assertions().assert_result(validation,response,status_code) # 调用该函数
1. 字符串包含断言( if str1 in str2)
- 功能说明:断言预期结果的字符串是否包含在接口的实际返回结果当中(模糊匹配),常用于:
- 消息提示(举例:message 包含"成功")
- Token 是否以 Bearer 开头
- 错误信息校验
- 代码实现:
common/assertion.py 中 字符串包含断言封装代码:
python
import jsonpath
from common.recordlog import logs
import allure
class 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 = 0
for assert_key,assert_value in value_dict.items():
if assert_key == 'code':
if str(assert_value) != str(status_code):
flag = flag + 1
allure.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 找不到时返回 False
if 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 + 1
allure.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 是空的)报错,直接停止测试
python
else:
resp_list = jsonpath.jsonpath(response,f'$..{assert_key}')
# 安全判断:jsonpath 找不到时返回 False
if not resp_list:
logs.error(f"字段 '{assert_key}' 在响应中未找到")
raise AssertionError(f"断言失败:字段 '{assert_key}' 不存在于响应中")
如果找到了则将之全部处理为字符串 ,方便后续使用 in
python
# 安全使用 resp_list[0]
if isinstance(resp_list[0],str):
resp_list = ''.join(resp_list)
else:
resp_list = str(resp_list[0])
关键代码!!包含 断言
python
if str(assert_value) in str(resp_list):
logs.info(f'字符串包含断言成功:预期结果:{assert_value}\n,实际结果:{resp_list}')
else:
flag = flag + 1
allure.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 中 结果相等断言封装代码:
python
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 0
else:
error_msg = f"相等断言失败: 期望={value}, 实际={filtered}"
logs.error(error_msg)
allure.attach(error_msg, "相等断言失败", allure.attachment_type.TEXT)
raise AssertionError(error_msg)
- 代码解析
检查两个参数是不是都是字典格式
python
if not isinstance(value, dict) or not isinstance(response, dict):
raise TypeError("预期结果和响应必须为字典")
从实际返回的数据 response 中,只取出你在 value 里提到的那些字段
python
filtered = {k: response[k] for k in value if k in response}
如果过滤后的结果和期望的一样,就打印日志:"成功",并返回 0 表示通过
python
if filtered == value:
logs.info(f"相等断言成功: {filtered}")
return 0
3. 结果不相等断言(if dict1 != dict2)
- 断言接口的实际返回结果是否与预期结果不一致(反向匹配),常用于:
- Token 刷新前后对比(新旧 token 不能相等)
- 验证码更新后内容变化校验(刷新后的验证码应不同于之前)
- 防重复提交场景(两次请求的唯一标识或时间戳不应相同)
- 代码实现:
common/assertion.py 中 结果相等断言封装代码:
python
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 0
else:
error_msg = f"不相等断言失败: {filtered} == {value} (不应相等)"
logs.error(error_msg)
allure.attach(error_msg, "不相等断言失败", allure.attachment_type.TEXT)
raise AssertionError(error_msg)
- 代码解析
检查两个参数是不是都是字典格式
python
if not isinstance(value, dict) or not isinstance(response, dict):
raise TypeError("预期结果和响应必须为字典")
从实际返回的数据 response 中,只取出你在 value 里提到的那些字段
python
filtered = {k: response[k] for k in value if k in response}
如果"实际" ≠ "期望",就打印日志:"成功",并返回 0 表示通过
python
if filtered != value:
logs.info(f"不相等断言成功: {filtered} ≠ {value}")
return 0
4. 断言接口返回值里面的任意一个值( if str1 in str2 )
- 在整个响应体中搜索是否包含某些关键词,适用于:
- 全文搜索用户信息(如:响应中包含 "张三")
- 校验 Token 或用户名出现在任意位置
- 错误信息中是否包含关键字(如 "参数错误")
- 代码实现:
common/assertion.py 中 结果相等断言封装代码:
python
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 = False
for val in all_values:
if str(expected) in str(val):
found = True
logs.info(f"找到匹配值: '{expected}' in '{val}'")
break
if 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 测试用户)
- 校验接口是否正确写入数据库
安装:
python
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库
报错:
python
RuntimeError: 'cryptography' package is required for sha256_password or caching_sha2_password auth methods
安装:
python
pip install cryptography
问题原因:
MySQL 服务器使用了 caching_sha2_password 作为用户认证方式(MySQL 8.0+ 的默认认证插件),而 PyMySQL 在这种模式下需要一个额外的加密库:cryptography
2) 前置准备(整体架构)
python
config.yaml 数据库配置
↓
operationConfig.py → 读取配置
↓
connection.py → 连接数据库 + 执行SQL
↓
assertion.py → 封装所有断言(包含db断言)
↓
测试用例中调用 Assertions().assert_result(...) → 自动完成所有验证
-
- config.yaml:数据库等配置驱动
yaml
# 数据库配置
mysql:
host: 192.192.2.6
port: 3306
user: fdafda
password: "fdakj@2023.."
database: ai_quality_control
charset: utf8mb4
-
- login.yaml:测试用例中的预期结果
yaml
validation:
- contains: {code: "200"}
- contains: {message: "成功"}
- eq: {'msg': '登录成功'}
- ne: {'msg': '登录失败'}
- db: 'select * from w_leaf where id = 1'
-
- operationConfig.py:通用配置读取工具(可直接获取到数据库配置)
python
import yaml
from conf.setting import FILE_PATH
class OperationYaml:
"""封装读取 YAML 配置文件"""
def __init__(self, file_path=None):
if file_path is None:
self.__file_path = FILE_PATH['conf']
else:
self.__file_path = file_path
self.__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) # 安全解析 YAML
except 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 None
data = self.__data
for key in keys:
if isinstance(data, dict) and key in data:
data = data[key]
else:
print(f"找不到路径: {keys}")
return None
return data
def get_envi(self, option):
"""快捷方法:获取接口环境地址"""
return self.get('api_envi', option)
def get_mysql_conf(self, key):
"""快捷方法:获取数据库配置"""
return self.get('mysql', key)
-
- connection.py:数据库操作封装文件
python
from common.recordlog import logs
from conf.operationConfig import OperationYaml
import pymysql
class 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 res
except Exception as e:
logs.error(e)
finally:
self.close()
def insert(self, sql):
"""新增"""
pass
def update(self, sql):
"""修改"""
pass
def delete(self, sql):
"""删除"""
pass
- 代码解释
使用 operationConfig.py 中封装好的解析方法读取 config.yaml 数据库配置
python
from conf.operationConfig import OperationYaml
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' # 防空
}
pymysql.connect 连接数据库, pymysql.cursors.DictCursor (以字典形式清晰展示返回内容)
python
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))
自动关闭数据库连接
python
def close(self):
if self.conn and self.cursor:
self.cursor.close()
self.conn.close()
封装数据库查询方法 (.cursor( ) 是操作数据库的"操作手柄")
python
def query(self, sql):
"""查询数据"""
try:
self.cursor.execute(sql)
self.conn.commit()
res = self.cursor.fetchall()
return res
except 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)数据库断言代码实现
python
from common.connection import ConnectMysql
def assert_mysql(self,expected_sql):
"""数据库断言
:param expected_sql:预期结果,也就是yaml文件的SQL语句
:return: 返回flag标识,0标识测试通过,非0表示测试失败
"""
flag = 0
conn = ConnectMysql()
db_value = conn.query(expected_sql)
if db_value is not None:
logs.info(f"数据库断言成功")
else:
flag = flag + 1
logs.error("数据库断言失败,请检查数据库是否存在该数据!")
return flag
终:assert_result:统一断言入口
assertion.py 文件
python
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 + flag
elif key == 'eq':
flag = self.equal_assert(value,response)
all_flag = all_flag + flag
elif key == 'ne':
flag = self.not_equal_assert(value,response)
all_flag = all_flag + flag
elif key == 'db':
flag = self.assert_mysql(value)
all_flag = all_flag + flag
assert all_flag == 0
logs.info('测试成功')
except Exception as e:
logs.error(f'测试失败,异常信息:{e}')
raise
python
→ 遍历每一项
→ 判断 key 是哪种断言
→ 调用对应方法
→ 返回 flag(0 或 1)
→ 累加到 all_flag
→ 最后 assert all_flag == 0 → 测试通过
- 外部使用方法
python
from common.assertion import Assertions # 导入工具
validation = to.pop('validation') # 获取 validation
Assertions().assert_result(validation,response,status_code) # 调用该函数