Pytest+requests进行接口自动化测试5.0(5种assert断言的封装 + pymysql)

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. 数据库断言)
    • 终: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(...) → 自动完成所有验证
    1. config.yaml:数据库等配置驱动
yaml 复制代码
# 数据库配置
mysql:
  host: 192.192.2.6
  port: 3306
  user: fdafda
  password: "fdakj@2023.."
  database: ai_quality_control
  charset: utf8mb4
    1. login.yaml:测试用例中的预期结果
yaml 复制代码
      validation:
        - contains: {code: "200"}
        - contains: {message: "成功"}
        - eq: {'msg': '登录成功'}
        - ne: {'msg': '登录失败'}
        - db: 'select * from w_leaf where id = 1'
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)
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) # 调用该函数
相关推荐
golang学习记2 小时前
从0死磕全栈第十天:nest.js集成prisma完成CRUD
开发语言·javascript·jvm
Familyism3 小时前
Java虚拟机——JVM
java·开发语言·jvm
java1234_小锋3 小时前
[免费]基于Python的深度学习音乐推荐系统(后端Django)【论文+源码+SQL脚本】
python·深度学习·django·音乐推荐·python音乐推荐
Biomamba生信基地3 小时前
挑战用R语言硬干一百万单细胞数据分析
开发语言·数据分析·r语言·生信·医药
九河云3 小时前
华为云 ELB:智慧负载均衡,让您的应用永葆流畅体验
运维·服务器·科技·华为云·负载均衡
烈风3 小时前
009 Rust函数
java·开发语言·rust
coding-fun3 小时前
SuperScript:C#脚本编辑器、C#脚本引擎
开发语言·c#·编辑器
B站_计算机毕业设计之家3 小时前
✅ 基于Scrapy与朴素贝叶斯的校园舆情监测与预警系统 Django+B/S架构 可视化大屏 机器学习
python·scrapy·架构·数据分析·django·情感分析·舆情分析
研來如此3 小时前
现有项目添加CMake
开发语言