Python+Pytest接口自动化项目
基于Python+pytest+sqlalchemy+requests+allure+jsonpath+yaml+Jenkins+Linux本项目基于 pytest + Allure 构建了一套完整、稳定、可扩展的接口自动化测试框架,适用于电商系统的业务流程测试。通过数据驱动、变量替换、断言机制、报告可视化等功能,有效提高了测试效率与质量。未来将持续优化,提升其通用性与智能化水平。
构建一个 Python +Pytest API 自动化测试框架
前言
- 🤔 每次都要手写复杂的 HTTP 请求代码
- 🤔 如何高效地管理数百个 API 测试用例
- 🤔 参数如何在多个接口间传递和依赖
- 🤔 如何快速定位测试失败原因
- 🤔 测试报告如何让非技术人员也能理解
可以通过基于 Python + Pytest + YAML 的 API 自动化测试框架来解决这些问题。
一、框架概览
1.1 框架特色一览
┌─────────────────────────────────────────────────────────┐
│ API 自动化测试框架核心特性 │
├─────────────────────────────────────────────────────────┤
│ ✅ 配置化驱动:YAML 文件定义测试用例,无需编写代码 │
│ ✅ 参数依赖管理:支持接口间参数传递,链式调用 │
│ ✅ 多种断言模式:contains、eq、ne、rv、db 数据库断言 │
│ ✅ 智能参数替换:${函数名(参数)} 动态参数替换 │
│ ✅ 数据库集成:支持 MySQL、MongoDB、ClickHouse │
│ ✅ 完整的报告系统:Allure + 钉钉通知 │
│ ✅ 自定义函数库:debugtalk.py 扩展能力 │
│ ✅ 企业级特性:文件上传、Cookie 管理、代理支持 │
└─────────────────────────────────────────────────────────┘
1.2 核心技术栈
| 技术组件 | 用途 | 特点 |
|---|---|---|
| Pytest | 测试框架 | 强大的 fixture、plugin、mark 机制 |
| Requests | HTTP 库 | 简洁API、完整的功能 |
| PyYAML | YAML 解析 | 配置化测试用例定义 |
| JSONPath | JSON 提取 | 灵活的数据提取能力 |
| SQLAlchemy | ORM | 支持多种数据库 |
| Allure | 测试报告 | 美观的 HTML 报告 |
| Pandas | 数据处理 | Excel 数据读写 |
| Faker | 数据生成 | 随机测试数据 |
二、架构设计
2.1 项目目录结构
python-pytest-/
├── base/ # 基础类封装层
│ ├── apiutil.py # ⭐ API 请求核心类
│ ├── apiutil_business.py # 业务相关的 API 操作
│ ├── generateId.py # ID 生成工具
│ ├── new_testcase_tools.py # UI 工具(生成测试用例)
│ └── removefile.py # 文件清理工具
│
├── common/ # 公共模块层
│ ├── sendrequest.py # HTTP 请求发送
│ ├── assertions.py # ⭐ 多种断言模式实现
│ ├── debugtalk.py # ⭐ 自定义函数库
│ ├── readyaml.py # YAML 读写
│ ├── recordlog.py # 日志记录
│ ├── connection.py # 数据库连接(MySQL、MongoDB)
│ ├── handleExcel.py # Excel 处理
│ ├── semail.py # 邮件发送
│ ├── dingRobot.py # 钉钉通知
│ ├── operxml.py # XML 处理
│ └── two_dimension_data.py # 二维数据处理
│
├── conf/ # 配置管理层
│ ├── setting.py # 全局配置
│ ├── config.ini # 环境配置
│ └── operationConfig.py # 配置操作类
│
├── data/ # 测试数据层
│ ├── testdata.xls # Excel 测试数据
│ ├── loginName.yaml # 登录用例
│ └── ...其他 YAML 用例
│
├── testcase/ # 测试用例集
│ ├── conftest.py # Pytest 配置
│ └── test_*.py # 测试文件
│
├── logs/ # 日志输出
├── report/ # 测试报告
│ ├── temp/ # Allure 原始数据
│ └── tmreport/ # 生成的报告
│
├── extract.yaml # ⭐ 参数提取文件(用于参数依赖)
├── environment.xml # 环境信息配置
├── pytest.ini # Pytest 配置
├── conftest.py # 全局 Pytest fixture
├── run.py # ⭐ 框架运行入口
├── requirements.txt # 项目依赖
└── README.md # 文档
2.2 分层架构
┌──────────────────────────────────────────────┐
│ Test Case Layer (Pytest + YAML) │
│ - 测试用例定义 │
│ - 配置化管理 │
│ - 参数数据驱动 │
└─────────────────┬──────────────────────────┘
│
┌─────────────────▼──────────────────────────┐
│ Business Layer (base/ + common/) │
│ - API 请求处理 (apiutil.py) │
│ - 参数替换和提取 (replace_load) │
│ - 多种断言模式 (assertions.py) │
│ - 自定义函数库 (debugtalk.py) │
│ - 数据库操作 (connection.py) │
│ - 日志和报告 (recordlog.py) │
└─────────────────┬──────────────────────────┘
│
┌─────────────────▼──────────────────────────┐
│ Infrastructure Layer │
│ - HTTP 请求 (requests library) │
│ - 数据库连接 (SQLAlchemy) │
│ - YAML 解析 (PyYAML) │
│ - 日志系统 (logging) │
│ - 报告生成 (Allure) │
└──────────────────────────────────────────────┘
三、核心特性深度解析
3.1 配置化测试用例定义
框架最大的特色是使用 YAML 文件定义测试用例,无需编写代码。
测试用例结构:
# data/轨迹查询.yaml
baseInfo:
api_name: 轨迹查询 # 接口名称(用于报告)
url: /monitor/vehicle/getMileageFrom # 接口路径
method: post # HTTP 方法
header:
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
token: ${get_extract_data(token)} # 参数替换(来自上一个接口)
userid: ${get_extract_data(userid)}
cookies:
SESSION: ${get_extract_data(Cookie,SESSION)}
testCase:
- case_name: 有效车牌号轨迹查询 # 用例名称
data: # 表单参数
vno: 鲁E00098
color: 2
startDate: ${start_time()} # 动态生成当前时间
endDate: ${end_time()}
ruleIds: ["${get_extract_data_lst(forbiddenRule,-2)}"]
files: # 文件上传
file: ./data/heimingdan.xlsx
validation: # 断言
- contains: {status_code: 200} # 状态码为 200
- contains: {'message':'success'} # 响应包含 success
- eq: {'state': '已入网'} # 状态字段等于
- ne: {'state': '已入网'} # 状态字段不等于
- rv: {"data": 2} # 任意值断言
- db: select * from sys_user where login_name='test999' # 数据库断言
extract: # 提取参数(用于后续接口)
id: $.data # JSONPath 提取
status: '"status":"(.*?)"' # 正则表达式提取
extract_list: # 提取列表参数
ids: $.result[*].id # 提取数组
优势对比:
# ❌ 传统方式:需要编写大量 Python 代码
def test_vehicle_track():
response = requests.post(
url='http://api.example.com/monitor/vehicle/getMileageFrom',
headers={
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
'token': get_token(),
'userid': get_userid()
},
cookies={'SESSION': get_session()},
data={
'vno': '鲁E00098',
'color': 2,
'startDate': datetime.now().strftime('%Y-%m-%d'),
'endDate': (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d'),
'ruleIds': [get_forbidden_rule()]
}
)
assert response.status_code == 200
assert 'success' in response.json()['message']
assert response.json()['state'] == '已入网'
# 提取参数
data = response.json()
save_extract_data('id', data['data'])
save_extract_data('status', re.search(r'"status":"(.*?)"', response.text).group(1))
# ✅ 使用框架:YAML 文件定义,一目了然
# 只需在 轨迹查询.yaml 中配置,无需代码
3.2 参数替换机制(核心创新)
这是框架最强大的特性——反射机制 + 动态参数替换。
工作原理:
YAML 中的参数占位符 → 正则识别 ${} → 提取函数名和参数 →
反射调用对应函数 → 获取返回值 → 替换原始值
实现代码(base/apiutil.py):
def replace_load(self, data):
"""yaml 数据替换解析"""
str_data = data
if not isinstance(data, str):
str_data = json.dumps(data, ensure_ascii=False)
# 循环处理所有 ${ } 表达式
for i in range(str_data.count('${')):
if '${' in str_data and '}' in str_data:
start_index = str_data.index('$')
end_index = str_data.index('}', start_index)
ref_all_params = str_data[start_index:end_index + 1]
# 提取函数名:从 ${ 到 (
func_name = ref_all_params[2:ref_all_params.index("(")]
# 提取参数:从 ( 到 )
func_params = ref_all_params[ref_all_params.index("(") + 1:ref_all_params.index(")")]
# 反射调用函数:相当于调用 DebugTalk().get_extract_data('token')
extract_data = getattr(DebugTalk(), func_name)(
*func_params.split(',') if func_params else ""
)
# 替换原始值
if extract_data and isinstance(extract_data, list):
extract_data = ','.join(e for e in extract_data)
str_data = str_data.replace(ref_all_params, str(extract_data))
# 还原数据类型
if data and isinstance(data, dict):
data = json.loads(str_data)
else:
data = str_data
return data
使用场景示例:
# 场景1:动态生成当前时间
startDate: ${start_time()}
endDate: ${end_time()}
# 场景2:获取上一个接口提取的参数
token: ${get_extract_data(token)}
userid: ${get_extract_data(userid)}
# 场景3:生成随机数据
phone: ${faker_phone()}
name: ${faker_name()}
# 场景4:从列表中获取指定索引
ruleId: ${get_extract_data_lst(forbiddenRule,-2)} # 获取列表的倒数第2个
# 场景5:自定义函数调用
custom_param: ${my_custom_function(arg1,arg2)}
自定义函数库(common/debugtalk.py):
class DebugTalk:
"""自定义函数库"""
def get_extract_data(self, key, *args):
"""获取提取的参数"""
# 从 extract.yaml 中读取
extract_data = self.read_yaml_data()
if args and args[0] == 'randoms':
# 从列表中随机获取一个
return random.choice(extract_data[key])
return extract_data.get(key)
def get_extract_data_lst(self, key, index=None):
"""获取列表参数"""
extract_data = self.read_yaml_data()
data = extract_data.get(key)
if index is not None:
return data[int(index)]
return data
def start_time(self):
"""获取当前时间"""
return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def end_time(self):
"""获取明天时间"""
tomorrow = datetime.now() + timedelta(days=1)
return tomorrow.strftime('%Y-%m-%d %H:%M:%S')
def faker_phone(self):
"""生成随机手机号"""
faker = Faker('zh_CN')
return faker.phone_number()
def faker_name(self):
"""生成随机姓名"""
faker = Faker('zh_CN')
return faker.name()
@staticmethod
def md5(text):
"""MD5 加密"""
import hashlib
return hashlib.md5(text.encode()).hexdigest()
@staticmethod
def base64_encode(text):
"""Base64 编码"""
import base64
return base64.b64encode(text.encode()).decode()
参数依赖示例:
# 第一个接口:登录,提取 token
# data/login.yaml
baseInfo:
api_name: 用户登录
url: /api/login
method: post
testCase:
- case_name: 用户登录
data:
username: admin
password: 123456
validation:
- contains: {status_code: 200}
extract:
token: $.data.token # 从响应提取 token
userid: $.data.userid
---
# 第二个接口:查询用户信息,使用 token
# data/user_info.yaml
baseInfo:
api_name: 查询用户信息
url: /api/user/info
method: get
header:
Authorization: Bearer ${get_extract_data(token)} # 使用上一个接口的 token
testCase:
- case_name: 查询用户信息
validation:
- contains: {status_code: 200}
执行流程:
1. 执行登录接口
- 发送请求
- 获取响应:{"data": {"token": "xxx", "userid": 123}}
- 提取参数:写入 extract.yaml
token: xxx
userid: 123
2. 执行查询接口
- 替换参数:get_extract_data(token) → xxx
- 请求头变为:Authorization: Bearer xxx
- 发送请求
3.3 五种断言模式
框架支持 5 种灵活的断言方式,满足不同场景需求。
1. Contains(包含断言)- 最常用
validation:
- contains: {status_code: 200} # 状态码为 200
- contains: {'message':'success'} # 响应包含 success
- contains: {'csrfToken': 'xxx'} # 验证 token 存在
- contains: {'data': None} # 数据为空
实现代码:
def contains_assert(self, value, response, status_code):
"""字符串包含断言"""
flag = 0
for assert_key, assert_value in value.items():
if assert_key == "status_code":
if assert_value != status_code:
flag += 1
logs.error(f"状态码{status_code}不等于{assert_value}")
else:
# 使用 JSONPath 从响应中提取值
resp_list = jsonpath.jsonpath(response, "$..%s" % assert_key)
if resp_list:
assert_value = None if assert_value.upper() == 'NONE' else assert_value
if assert_value in resp_list:
logs.info(f"包含断言成功")
else:
flag += 1
logs.error(f"包含断言失败")
return flag
2. Eq(相等断言)
validation:
- eq: {'state': '已入网'} # 状态字段等于指定值
- eq: {'count': 10} # 数量等于 10
3. Ne(不相等断言)
validation:
- ne: {'status': 'error'} # 状态不是 error
- ne: {'code': 400} # 状态码不是 400
4. Rv(响应值断言)- 验证任意字段
validation:
- rv: {"data": 2} # 验证响应中 data 字段值为 2
- rv: {"count": 0} # 验证返回记录数为 0
5. Db(数据库断言)- 企业级特性
validation:
- db: select * from sys_user where login_name='test999'
# SQL 查询结果不为空则断言成功
# 可用于验证数据是否正确写入数据库
实现代码:
def assert_mysql_data(self, expected_results):
"""数据库断言"""
flag = 0
conn = ConnectMysql()
db_value = conn.query_all(expected_results) # 执行 SQL
if db_value is not None:
logs.info("数据库断言成功")
else:
flag += 1
logs.error("数据库断言失败,请检查数据库是否存在该数据")
return flag
完整断言示例:
testCase:
- case_name: 完整的断言示例
data:
username: admin
password: 123456
validation:
# 必须先验证状态码
- contains: {status_code: 200}
# 然后验证消息
- contains: {'message':'success'}
# 验证数据字段
- eq: {'state': '已入网'}
# 验证状态不是错误
- ne: {'error': 'exists'}
# 验证响应中的任意值
- rv: {"data": 1}
# 验证数据是否写入数据库
- db: select * from sys_user where id=1
断言执行逻辑:
def assert_result(self, expected, response, status_code):
"""
断言主方法
- 多个断言顺序执行
- 使用 all_flag 累计失败次数
- 只要有一个断言失败,整个测试用例失败
"""
all_flag = 0
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 == 'rv':
flag = self.assert_response_any(response, value)
all_flag = all_flag + flag
elif key == 'db':
flag = self.assert_mysql_data(value)
all_flag = all_flag + flag
# 所有断言都通过(all_flag == 0)才算成功
if all_flag == 0:
logs.info("测试成功")
assert True
else:
logs.error("测试失败")
assert False
3.4 数据库集成(企业级特性)
框架原生支持多种数据库,可以进行数据预置、数据清理、数据库断言。
支持的数据库:
# common/connection.py
class ConnectMysql:
"""MySQL 连接"""
def query_one(self, sql) # 查询单条
def query_all(self, sql) # 查询多条
def insert(self, sql) # 插入数据
def delete(self, sql) # 删除数据
def update(self, sql) # 更新数据
class ConnectMongodb:
"""MongoDB 连接"""
# 支持 MongoDB 的所有基本操作
class ClickhouseSQL:
"""ClickHouse 连接"""
# 支持 ClickHouse 的数据查询
使用场景:
# 场景1:测试前预置数据
def setup(self):
conn = ConnectMysql()
# 插入测试数据
sql = "insert into test_users values (1, 'test_user', 'test@example.com')"
conn.insert(sql)
# 场景2:断言数据库数据
validation:
- db: select * from test_users where username='test_user'
# 场景3:测试后清理数据
def teardown(self):
conn = ConnectMysql()
sql = "delete from test_users where username='test_user'"
conn.delete(sql)
Conftest 中的数据清理示例:
# testcase/conftest.py
@pytest.fixture(scope='session', autouse=True)
def datadb_init():
"""
后置处理:测试后清理数据
这样不会产生脏数据,也不会对系统造成影响
"""
yield # 测试执行
# 测试完成后清理
conn = ConnectMysql()
sql = "delete from test_users where username like 'test_%'"
conn.delete(sql)
allure.attach('将测试数据清空', 'fixture后置', allure.attachment_type.TEXT)
3.5 参数提取与数据处理
框架支持两种参数提取方式:JSONPath 和正则表达式。
JSON 提取(推荐):
extract:
id: $.data # 提取 data 字段
name: $.data.user.name # 提取嵌套字段
status: $.status # 提取顶级字段
extract_list:
ids: $.result[*].id # 提取数组中所有 id
names: $.users[0:3].name # 提取前3个用户的名称
正则提取:
extract:
status: '"status":"(.*?)"' # 提取 status 的值
token: 'token":"([^"]*)"' # 提取 token 的值
data: 'data":(\d*)' # 提取数字
extract_list:
ids: 'id":"(\d+)"' # 提取所有 id(列表形式)
实现代码:
def extract_data(self, testcase_extarct, response):
"""参数提取"""
try:
for key, value in testcase_extarct.items():
# 处理 JSON 提取
if '$' in value:
ext_json = jsonpath.jsonpath(json.loads(response), value)[0]
if ext_json:
extract_data = {key: ext_json}
logs.info(f'提取接口的返回值:{extract_data}')
self.read.write_yaml_data(extract_data) # 写入 extract.yaml
# 处理正则表达式提取
pattern_lst = ['(.*?)', '(.+?)', r'(\d)', r'(\d*)']
for pat in pattern_lst:
if pat in value:
ext_lst = re.search(value, response)
if ext_lst:
if pat in [r'(\d+)', r'(\d*)']:
extract_data = {key: int(ext_lst.group(1))}
else:
extract_data = {key: ext_lst.group(1)}
self.read.write_yaml_data(extract_data)
except Exception as e:
logs.error(e)
3.6 文件上传处理
框架原生支持文件上传功能。
testCase:
- case_name: 导入数据文件
data:
import_type: user_list
files: # 文件上传
file: ./data/user_list.xlsx # 相对路径或绝对路径
validation:
- contains: {status_code: 200}
- contains: {'message': 'success'}
实现代码:
def specification_yaml(self, base_info, test_case):
"""处理接口请求"""
# 处理文件上传接口
file, files = test_case.pop('files', None), None
if file is not None:
for fk, fv in file.items():
allure.attach(json.dumps(file), '导入文件')
# 打开文件并传递给 requests
files = {fk: open(fv, mode='rb')}
# 发送请求(包含文件)
res = self.run.run_main(
name=api_name,
url=url,
case_name=case_name,
header=header,
method=method,
file=files, # 文件参数
cookies=cookie,
**test_case
)
3.7 完整的测试报告系统
框架集成了企业级的报告系统。
Allure 报告配置:
# run.py
if REPORT_TYPE == 'allure':
pytest.main([
'-s', '-v',
'--alluredir=./report/temp',
'./testcase',
'--clean-alluredir',
'--junitxml=./report/results.xml'
])
# 复制环境信息配置文件
shutil.copy('./environment.xml', './report/temp')
# 生成并打开报告
os.system(f'allure serve ./report/temp')
环境信息配置(environment.xml):
<?xml version="1.0" encoding="UTF-8"?>
<environment>
<parameter>
<name>测试环境</name>
<value>测试服务器</value>
</parameter>
<parameter>
<name>API 基础 URL</name>
<value>http://api.example.com</value>
</parameter>
<parameter>
<name>数据库</name>
<value>MySQL 8.0</value>
</parameter>
<parameter>
<name>测试框架</name>
<value>Pytest + Python</value>
</parameter>
</environment>
钉钉通知集成:
# conftest.py
from common.dingRobot import send_dd_msg
def pytest_terminal_summary(terminalreporter, exitstatus, config):
"""测试完成后发送钉钉通知"""
total = terminalreporter._numcollected
passed = len(terminalreporter.stats.get('passed', []))
failed = len(terminalreporter.stats.get('failed', []))
error = len(terminalreporter.stats.get('error', []))
summary = f"""
自动化测试结果,请查看
测试用例总数:{total}
测试通过数:{passed}
测试失败数:{failed}
错误数量:{error}
"""
# 发送钉钉通知
if dd_msg: # 配置中开启钉钉通知
send_dd_msg(summary)
四、最佳实践与应用场景
4.1 场景一:链式 API 调用(参数依赖)
# 场景:用户注册 → 用户登录 → 查询用户信息 → 修改用户信息
# 1. 用户注册 - data/register.yaml
baseInfo:
api_name: 用户注册
url: /api/register
method: post
testCase:
- case_name: 注册新用户
data:
username: ${faker_name()}
password: 123456
email: ${faker_email()}
validation:
- contains: {status_code: 200}
extract:
user_id: $.data.id # 提取用户 ID
username: $.data.username # 提取用户名
token: $.data.token # 提取 token
---
# 2. 用户登录 - data/login.yaml
baseInfo:
api_name: 用户登录
url: /api/login
method: post
header:
Content-Type: application/json
testCase:
- case_name: 使用新注册的账号登录
data:
username: ${get_extract_data(username)} # 使用注册时的用户名
password: 123456
validation:
- contains: {status_code: 200}
extract:
token: $.data.token # 提取登录 token
---
# 3. 查询用户信息 - data/user_info.yaml
baseInfo:
api_name: 查询用户信息
url: /api/users/${get_extract_data(user_id)}
method: get
header:
Authorization: Bearer ${get_extract_data(token)}
testCase:
- case_name: 查询新注册用户的信息
validation:
- contains: {status_code: 200}
- contains: {'username': '${get_extract_data(username)}'}
---
# 4. 修改用户信息 - data/update_user.yaml
baseInfo:
api_name: 修改用户信息
url: /api/users/${get_extract_data(user_id)}
method: put
header:
Authorization: Bearer ${get_extract_data(token)}
Content-Type: application/json
testCase:
- case_name: 修改用户信息
data:
email: newemail@example.com
phone: ${faker_phone()}
validation:
- contains: {status_code: 200}
- contains: {'message': 'success'}
4.2 场景二:异常场景测试(多断言)
# 数据验证 - data/user_validation.yaml
baseInfo:
api_name: 创建用户
url: /api/users
method: post
header:
Content-Type: application/json
testCase:
# 场景1:用户名为空
- case_name: 用户名为空
data:
username: ""
password: 123456
validation:
- contains: {status_code: 400}
- contains: {'error': 'username required'}
# 场景2:用户名过长
- case_name: 用户名过长(超过 50 字符)
data:
username: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"
password: 123456
validation:
- contains: {status_code: 400}
- contains: {'error': 'username too long'}
# 场景3:密码过弱
- case_name: 密码过弱
data:
username: newuser
password: 123
validation:
- contains: {status_code: 400}
- contains: {'error': 'password too weak'}
# 场景4:用户已存在
- case_name: 用户名已存在
data:
username: admin
password: 123456
validation:
- contains: {status_code: 409}
- contains: {'error': 'user already exists'}
- db: select * from users where username='admin'
4.3 场景三:数据库相关测试
# data/order_business.yaml
baseInfo:
api_name: 创建订单业务
url: /api/orders
method: post
testCase:
- case_name: 创建订单后验证数据库
data:
user_id: 1
product_id: 101
quantity: 5
price: 99.99
validation:
# 验证接口响应
- contains: {status_code: 201}
- contains: {'message': 'order created'}
# 验证数据库中订单是否创建
- db: select * from orders where product_id=101
# 验证库存是否扣减
- db: select quantity from products where id=101
4.4 场景四:文件上传与数据导入
# data/import_users.yaml
baseInfo:
api_name: 批量导入用户
url: /api/users/import
method: post
testCase:
- case_name: 导入用户列表 Excel 文件
files:
file: ./data/users_import.xlsx
data:
import_type: user_list
skip_errors: true
validation:
- contains: {status_code: 200}
- contains: {'message': 'import success'}
# 验证导入记录数
- rv: {'imported_count': 100}
# 验证数据库中用户数量
- db: select count(*) from users where created_date=today()
五、框架代码走查
5.1 RequestBase 类 - 核心请求处理
# base/apiutil.py
class RequestBase:
"""API 请求基础类 - 框架核心"""
def __init__(self):
self.run = SendRequest()
self.conf = OperationConfig()
self.read = ReadYamlData()
self.asserts = Assertions()
def replace_load(self, data):
"""参数替换 - 处理所有 ${ } 表达式"""
# 识别、提取、替换参数
pass
def specification_yaml(self, base_info, test_case):
"""
接口请求处理 - 主方法
流程:
1. 替换参数
2. 发送请求
3. 提取数据
4. 执行断言
"""
# 1. 处理请求头和 Cookie
header = self.replace_load(base_info['header'])
cookie = eval(self.replace_load(base_info['cookies']))
# 2. 处理请求参数
for key, value in test_case.items():
if key in ['data', 'json', 'params']:
test_case[key] = self.replace_load(value)
# 3. 处理文件上传
if 'files' in test_case:
files = {fk: open(fv, mode='rb') for fk, fv in test_case['files'].items()}
# 4. 发送请求
res = self.run.run_main(
url=url,
method=method,
header=header,
cookies=cookie,
file=files,
**test_case
)
# 5. 提取参数
extract = test_case.get('extract')
if extract:
self.extract_data(extract, res.text)
# 6. 执行断言
validation = test_case.get('validation')
if validation:
self.asserts.assert_result(validation, res.json(), res.status_code)
def extract_data(self, testcase_extract, response):
"""参数提取 - 支持 JSONPath 和正则"""
for key, value in testcase_extract.items():
if '$' in value:
# JSONPath 提取
ext_json = jsonpath.jsonpath(json.loads(response), value)[0]
self.read.write_yaml_data({key: ext_json})
else:
# 正则提取
ext_lst = re.search(value, response)
self.read.write_yaml_data({key: ext_lst.group(1)})
5.2 Assertions 类 - 多种断言实现
# common/assertions.py
class Assertions:
"""断言类 - 支持 5 种断言模式"""
def assert_result(self, expected, response, status_code):
"""主断言方法"""
all_flag = 0
for yq in expected:
for key, value in yq.items():
if key == "contains":
flag = self.contains_assert(value, response, status_code)
elif key == "eq":
flag = self.equal_assert(value, response)
elif key == 'ne':
flag = self.not_equal_assert(value, response)
elif key == 'rv':
flag = self.assert_response_any(response, value)
elif key == 'db':
flag = self.assert_mysql_data(value)
all_flag += flag
# 所有断言都必须通过
assert all_flag == 0, "测试失败"
def contains_assert(self, value, response, status_code):
"""包含断言"""
# 实现逻辑...
pass
def equal_assert(self, value, response):
"""相等断言"""
# 实现逻辑...
pass
def assert_mysql_data(self, sql):
"""数据库断言"""
conn = ConnectMysql()
result = conn.query_all(sql)
return 0 if result else 1
5.3 DebugTalk 类 - 自定义函数库
# common/debugtalk.py
class DebugTalk:
"""自定义函数库 - 用于参数替换"""
def get_extract_data(self, key, *args):
"""获取提取的参数"""
extract_data = ReadYamlData().read_yaml_data()
if args and args[0] == 'randoms':
# 从列表中随机取
return random.choice(extract_data[key])
if args:
# 支持多层级访问
return extract_data[key][args[0]]
return extract_data.get(key)
def start_time(self):
"""当前时间"""
return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def end_time(self):
"""明天时间"""
tomorrow = datetime.now() + timedelta(days=1)
return tomorrow.strftime('%Y-%m-%d %H:%M:%S')
def faker_phone(self):
"""生成随机手机号"""
return Faker('zh_CN').phone_number()
def faker_email(self):
"""生成随机邮箱"""
return Faker().email()
@staticmethod
def md5(text):
"""MD5 加密"""
return hashlib.md5(text.encode()).hexdigest()
六、框架的安装与使用
6.1 快速安装
# 克隆项目
git clone https://github.com/ismezed/python-pytest-.git
cd python-pytest-
# 安装依赖(使用国内镜像加速)
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
# 验证安装
pytest --version
6.2 项目配置
# conf/config.ini
[api_envi]
host = http://127.0.0.1:8080
timeout = 60
[mysql]
host = 127.0.0.1
port = 3306
user = root
password = 123456
database = test_db
[mongodb]
host = 127.0.0.1
port = 27017
database = test_db
6.3 运行测试
# 运行所有测试
python run.py
# 或使用 Pytest 命令
pytest ./testcase -v --alluredir=./reports
# 生成并打开 Allure 报告
allure serve ./reports
# 运行特定测试文件
pytest ./testcase/test_login.py -v
# 运行特定测试用例
pytest ./testcase/test_login.py::TestLogin::test_login_success -v
# 使用 mark 运行指定标记的用例
pytest -m smoke ./testcase -v
6.4 查看 Allure 报告
# 自动生成并打开报告
allure serve ./report/temp
# 或手动生成报告
allure generate ./report/temp -o ./allure_report --clean
# 打开 allure_report/index.html
七、常见问题与调试技巧
7.1 问题一:参数替换失败
现象: 参数替换后仍然是原始值 ${get_extract_data(token)}
原因: 函数未定义或参数错误
解决方案:
# 1. 检查 debugtalk.py 中是否定义了函数
class DebugTalk:
def get_extract_data(self, key):
pass # ✅ 确保函数存在
# 2. 检查 extract.yaml 中是否有对应的数据
# extract.yaml
token: "xxx_token_value"
# 3. 添加日志调试
def replace_load(self, data):
print(f"替换前:{data}")
# 替换逻辑
print(f"替换后:{data}")
7.2 问题二:断言失败但看不到详细原因
现象: 测试失败,但不知道期望值和实际值是什么
解决方案:
# 在 assertions.py 中添加详细日志
def contains_assert(self, value, response, status_code):
flag = 0
for assert_key, assert_value in value.items():
# 输出详细信息
print(f"期望值:{assert_value}")
print(f"实际值:{response}")
# 使用日志记录
logs.info(f"期望:{assert_value},实际:{response}")
7.3 问题三:数据库连接失败
现象: 数据库断言报错 Connection refused
解决方案:
# 1. 检查配置
# conf/config.ini
[mysql]
host = 127.0.0.1
port = 3306
user = root
password = 123456 # ✅ 检查密码是否正确
# 2. 检查 MySQL 是否运行
mysql -u root -p -h 127.0.0.1
# 3. 检查防火墙
# 确保 3306 端口开放
7.4 问题四:文件上传路径问题
现象: 文件上传失败 FileNotFoundError
解决方案:
# ✅ 使用相对路径(相对于项目根目录)
files:
file: ./data/users.xlsx
# ✅ 或使用绝对路径
files:
file: /home/user/project/data/users.xlsx
# 不要使用以下方式
files:
file: ./testcase/../data/users.xlsx # ❌ 复杂的相对路径
八、性能优化与最佳实践
8.1 性能优化建议
1. 使用连接池
# common/sendrequest.py
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
class SendRequest:
def __init__(self):
self.session = requests.Session()
# 配置重试策略
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504]
)
# 使用连接池
adapter = HTTPAdapter(
max_retries=retry_strategy,
pool_connections=10,
pool_maxsize=20
)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
2. 并行执行测试
# 安装 pytest-xdist
pip install pytest-xdist
# 4 个进程并行执行
pytest -n 4 ./testcase
# 自动检测 CPU 核数
pytest -n auto ./testcase
3. 使用 Mock 数据
# 避免真实数据库操作
validation:
# ❌ 不好的做法:每次都查询数据库
- db: select * from users where id=1
# ✅ 好的做法:Mock 响应数据
- rv: {"user_id": 1, "name": "test"}
8.2 最佳实践
1. 合理组织测试用例
data/
├── login/ # 登录相关接口
│ ├── login.yaml
│ ├── logout.yaml
│ └── refresh_token.yaml
├── user/ # 用户相关接口
│ ├── create_user.yaml
│ ├── update_user.yaml
│ └── delete_user.yaml
└── order/ # 订单相关接口
├── create_order.yaml
├── query_orders.yaml
└── cancel_order.yaml
2. 使用环境变量管理配置
# conf/setting.py
import os
# 从环境变量读取配置
API_HOST = os.getenv('API_HOST', 'http://127.0.0.1:8080')
DB_HOST = os.getenv('DB_HOST', '127.0.0.1')
DB_PORT = int(os.getenv('DB_PORT', '3306'))
# 这样可以支持不同的环境
# 开发环境:export API_HOST=http://dev.example.com
# 测试环境:export API_HOST=http://test.example.com
# 生产环境:export API_HOST=http://prod.example.com
3. 添加测试标记
# conftest.py
import pytest
@pytest.mark.smoke # 冒烟测试
@pytest.mark.regression # 回归测试
@pytest.mark.performance # 性能测试
# 运行指定标记的用例
pytest -m smoke ./testcase
pytest -m "not slow" ./testcase
4. 详细的日志记录
# common/recordlog.py
import logging
logger = logging.getLogger(__name__)
logger.info(f"发送请求:{method} {url}")
logger.info(f"请求头:{header}")
logger.info(f"请求体:{data}")
logger.info(f"响应状态码:{status_code}")
logger.info(f"响应体:{response}")
logger.info(f"提取参数:{extract_data}")
九、总结
框架核心优势
| 特性 | 优势 | 应用场景 |
|---|---|---|
| 配置化测试 | 无需代码,YAML 定义 | 降低学习成本 |
| 参数依赖管理 | 支持接口间参数传递 | 复杂业务流程测试 |
| 多种断言模式 | 灵活的断言方式 | 各种场景验证 |
| 数据库集成 | 原生支持多种数据库 | 数据一致性验证 |
| 智能参数替换 | 反射机制动态替换 | 动态测试数据生成 |
| 完整报告系统 | Allure + 钉钉通知 | 测试结果追踪 |
适用场景
✅ API 功能测试 - 验证接口功能正确性
✅ 集成测试 - 多个接口的业务流程测试
✅ 回归测试 - 变更后的影响范围测试
✅ 性能测试 - 接口响应时间验证
✅ 数据验证 - 数据库数据一致性验证
✅ 异常测试 - 异常场景和边界值测试
与其他框架的对比
| 框架 | 优势 | 劣势 |
|---|---|---|
| 本框架(YAML+Pytest) | 配置化、无代码、易维护 | 学习曲线,需要了解 Pytest |
| Postman | UI 友好、易上手 | 无参��依赖、难以维护大量用例 |
| Robot Framework | 关键词驱动、功能全 | 过于复杂、学习成本高 |
| Rest Assured(Java) | 类型安全、性能好 | 需要编程、维护成本高 |
十、快速开始模板
最小化示例
# data/api_example.yaml
baseInfo:
api_name: 获取用户信息
url: /api/users/1
method: get
header:
Content-Type: application/json
testCase:
- case_name: 获取用户信息成功
validation:
- contains: {status_code: 200}
- contains: {'id': 1}
- rv: {'name': 'admin'}
完整示例
# data/user_management.yaml
baseInfo:
api_name: 用户管理
url: /api/users
method: post
header:
Content-Type: application/json
Authorization: Bearer ${get_extract_data(token)}
testCase:
# 创建用户
- case_name: 创建新用户
data:
name: ${faker_name()}
email: ${faker_email()}
age: 18
validation:
- contains: {status_code: 201}
- contains: {'message': 'success'}
extract:
user_id: $.data.id
# 查询用户
- case_name: 查询新创建的用户
data:
id: ${get_extract_data(user_id)}
validation:
- contains: {status_code: 200}
- rv: {'id': ${get_extract_data(user_id)}}
参考资源
- 📚 项目 GitHub
- 📚 Pytest 官方文档
- 📚 JSONPath 在线测试
- 📚 Allure 官方文档
结语: API 自动化测试是保证 API 质量的重要手段。一个好的测试框架应该让写测试变得简单,这个框架正是为此而设计的。
更多推荐
所有评论(0)