Python+Pytest打造高维护性接口测试框架:哆哆来商城POM架构与断言实践
一.项目介绍
这是一个经典的电商业务场景,基于Python的接口自动化测试框架,该项目技术栈包括:Python+pytest+sqlalchemy+requests+allure+jsonpath+yaml+Jenkins+Linux
该项目是一个在线购物的商城网站,包括用户注册,登录,下单,上架/下架商品,下单支付等相关功能。
二.项目结构说明
1、项目整体框架
本项目采用清晰的分层架构,将核心操作、工具方法、测试用例和数据彻底分离。这种“高内聚、低耦合”的设计,极大地提升了框架的可维护性和可扩展性。核心目录结构如下:
project-root/ # 项目根目录
├─ base/ # 基础类封装(核心功能)
│ ├─ api_request.py # 接口请求基类(封装requests)
│ ├─ test_case_tools.py # 测试用例工具类(如数据处理、断言)
│ └─ driver.py # 浏览器驱动封装(可选,UI自动化用)
├─ common/ # 公共方法封装(可复用工具)
│ ├─ log_utils.py # 日志处理工具
│ ├─ excel_parser.py # Excel数据解析工具
│ ├─ yaml_parser.py # YAML数据解析工具
│ └─ db_operation.py # 数据库操作工具(如MySQL)
├─ conf/ # 全局配置目录
│ ├─ config.ini # 环境配置文件(API地址、账号等)
│ ├─ allure_config.yml # Allure报告自定义配置
│ └─ env.py # 环境变量管理文件
├─ data/ # 测试数据目录
│ ├─ api_data/ # 接口测试数据
│ │ ├─ login_data.yaml # 登录接口测试用例数据
│ │ └─ order_data.xlsx # 订单接口Excel数据
│ └─ ui_data/ # UI自动化测试数据(可选)
│ └─ page_elements.yaml # 页面元素定位数据
├─ logs/ # 测试日志目录(自动生成)
│ └─ test_20231001.log # 按日期命名的日志文件
├─ report/ # 测试报告目录
│ ├─ allure_html/ # Allure交互式报告(自动生成)
│ ├─ tm_report/ # TMReport表格报告(自动生成)
│ └─ report_config.py # 报告生成配置文件
├─ testcase/ # 测试用例目录
│ ├─ api_test/ # 接口测试用例
│ │ ├─ test_login.py # 登录接口测试类
│ │ └─ test_order.py # 订单接口测试类
│ └─ ui_test/ # UI自动化测试用例(可选)
│ └─ test_homepage.py # 首页UI测试类
├─ venv/ # 虚拟环境目录(自动生成)
├─ conftest.py # pytest全局钩子文件(固定名称)
├─ environment.xml # Allure报告环境信息文件
├─ extract.yaml # 接口依赖参数存储文件
├─ pytest.ini # pytest配置文件(固定名称)
├─ requirements.txt # 第三方库依赖清单
└─ run.py # 主程序入口(执行测试和生成报告)
2、驱动层 +用例层+数据层
本自动化测试项目框架拆分成三层:驱动层 +用例层+数据层。
驱动层:通俗来讲(怎么做)。提供技术实现和业务操作封装。主要体现在 base/ 目录下。
用例层 : (做什么)。组织测试场景,描述业务流程。testcases/ 目录下。
数据层 :(用什么数据做)。存储和管理测试数据。data/ 目录下的YAML/JSON/Excel文件。
三.核心代码
1. 程序入口 run.py
项目通过 run.py 作为统一入口执行测试,支持生成Allure报告并自动打开浏览器查看。核心代码如下:
import shutil
import pytest
import os
import webbrowser
from conf.setting import REPORT_TYPE
if __name__ == '__main__':
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')
elif REPORT_TYPE == 'tm':
pytest.main(['-vs', '--pytest-tmreport-name=testReport.html', '--pytest-tmreport-path=./report/tmreport'])
webbrowser.open_new_tab(os.getcwd() + '/report/tmreport/testReport.html')
代码说明:
主程序入口
# run.py - 测试框架主入口
if __name__ == '__main__':
# 当脚本作为主程序运行时,执行测试流程
# 此条件判断防止模块被导入时意外执行测试逻辑
完整的入口程序不仅包含执行逻辑,还需要处理不同类型的报告生成。下面是针对Allure报告的详细配置:
Allure 报告处理逻辑
if REPORT_TYPE == 'allure':
pytest.main([
'-s', '-v', # 输出详细日志
'--alluredir=./report/temp', # 指定Allure原始数据目录
'./testcase', # 指定测试用例目录
'--clean-alluredir', # 清理历史数据
'--junitxml=./report/results.xml' # 同时生成JUnit格式报告
])
为增强测试报告的可追溯性,框架会将测试环境信息注入Allure报告。这在多环境测试中尤为重要,可以快速确认测试执行的具体环境。下面是具体说明:
# 复制环境信息文件至Allure原始数据目录
shutil.copy('./config/environment.xml', './report/temp/')
# 作用:Allure报告会读取此文件,在[Environment]板块展示测试环境信息
# 内容示例:<environment><parameter><key>Python</key><value>3.9</value></parameter></environment>
# 生成并展示Allure报告
return_code = os.system('allure serve ./report/temp')
# 命令解析:
# 1. `allure serve`:启动本地Web服务并渲染Allure报告
# 2. `./report/temp`:指定包含Allure原始数据的目录
# 3. 操作系统会自动调用默认浏览器打开报告页面
在持续集成流水线中,通常使用 allure generate 命令生成静态HTML报告并归档,而非使用 allure serve。本项目的 serve 命令主要用于测试调试阶段的即时查看。
2.测试用例 testcase
商务管理代码示例:
本模块位于 testcases/ 层,是POM模式中的“用例层”,职责是描述业务场景,而非实现具体操作。通过调用驱动层(base/)封装,将YAML中描述的测试数据转化为可执行的业务流程验证。
import allure
import pytest
from common.readyaml import get_testcase_yaml
from base.apiutil_business import RequestBase
from base.generateId import m_id, c_id
# 注意:业务场景的接口测试要调用base目录下的apiutil_business文件
@allure.feature(next(m_id) + '电子商务管理系统(业务场景)')
class TestEBusinessScenario:
@allure.story(next(c_id) + '商品列表到下单支付流程')
@pytest.mark.parametrize('case_info', get_testcase_yaml('./testcase/Business interface/BusinessScenario.yml'))
def test_business_scenario(self, case_info):
allure.dynamic.title(case_info['baseInfo']['api_name'])
RequestBase().specification_yaml(case_info)
代码说明:
@pytest.mark.parametrize 参数化装饰器
@pytest.mark.parametrize('case_info', get_testcase_yaml('./testcase/Business interface/BusinessScenario.yml'))
作用:
实现参数化测试,即一个测试方法可以运行多组不同的输入数据,数据储存在YAML文件中。@pytest.mark.parametrize 是项目框架实现 “数据驱动” 的核心技术。它把测试逻辑(怎么写)和测试数据(测什么)分开。pytest运行时会将这一个方法定义,基于YAML中的数据量自动展开为多个独立的测试用例执行,每个都有完整Allure报告。
每一组 case_info 数据都会触发一次完整的测试执行。
示例(YAML 文件内容):
- baseInfo:
api_name: 获取商品列表
method: GET
url: /product/list
- baseInfo:
api_name: 提交订单
method: POST
url: /order/create
测试方法定义
### 测试方法定义
```python
def test_business_scenario(self, case_info):
这是一个 pytest 测试方法,每个参数化的 case_info 都会触发一次该方法的执行。
self 表示这是类中的一个实例方法(属于 TestEBusinessScenario 类)。
case_info 是从 YAML 文件中加载的一条测试用例的数据字典。
动态设置 Allure 报告标题
allure.dynamic.title(case_info['baseInfo']['api_name'])
标题内容来自于 YAML 文件中 baseInfo.api_name 字段
如果 api_name 是 "用户下单成功",那么在Allure报告中显示的用例标题就是"用户下单成功"。
执行接口请求和校验
RequestBase().specification_yaml(case_info)
这个代码是POM模式中的核心调用,体现了框架的重点——封装、可维护性
用例层(这里)只负责下达指令:命令RequestBase()按照提供的case_info(从YAML中读取的测试场景数据),完成从构造请求到验证结果的全套工序。
驱动层(RequestBase)负责具体操作:构造请求、发送请求、处理Cookie、记录日志、解析响应、执行断言。(注:读取case_info,并根据其中的配置(如 URL、方法、请求头、预期结果等)发送 HTTP 请求。)
优势:将接口测试技术细节封装在驱动层。当技术栈或协议细节变更时,仅需修改底层驱动,所有上层用例无需任何改动,极大提升了框架的可维护性。(如:当HTTP客户端库从 requests 更换为 httpx,只需修改 RequestBase类,测试用例完全不需要改动。)
3.接口测试 specification_yaml() 方法
创建 RequestBase 实例后,就可以调用其 specification_yaml() 方法处理YAML测试场景。
代码示例
def specification_yaml(self, base_info, test_case):
"""
接口请求处理基本方法
:param base_info: yaml文件里面的baseInfo
:param test_case: yaml文件里面的testCase
:return:
"""
try:
params_type = ['data', 'json', 'params']
url_host = self.conf.get_section_for_data('api_envi', 'host')
api_name = base_info['api_name']
allure.attach(api_name, f'接口名称:{api_name}', allure.attachment_type.TEXT)
url = url_host + base_info['url']
allure.attach(api_name, f'接口地址:{url}', allure.attachment_type.TEXT)
method = base_info['method']
allure.attach(api_name, f'请求方法:{method}', allure.attachment_type.TEXT)
header = self.replace_load(base_info['header'])
allure.attach(api_name, f'请求头:{header}', allure.attachment_type.TEXT)
# 处理cookie
cookie = None
if base_info.get('cookies') is not None:
cookie = eval(self.replace_load(base_info['cookies']))
case_name = test_case.pop('case_name')
allure.attach(api_name, f'测试用例名称:{case_name}', allure.attachment_type.TEXT)
# 处理断言
val = self.replace_load(test_case.get('validation'))
test_case['validation'] = val
validation = eval(test_case.pop('validation'))
# 处理参数提取
extract = test_case.pop('extract', None)
extract_list = test_case.pop('extract_list', None)
# 处理接口的请求参数
for key, value in test_case.items():
if key in params_type:
test_case[key] = self.replace_load(value)
# 处理文件上传接口
file, files = test_case.pop('files', None), None
if file is not None:
for fk, fv in file.items():
allure.attach(json.dumps(file), '导入文件')
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)
status_code = res.status_code
allure.attach(self.allure_attach_response(res.json()), '接口响应信息', allure.attachment_type.TEXT)
try:
res_json = json.loads(res.text) # 把json格式转换成字典字典
if extract is not None:
self.extract_data(extract, res.text)
if extract_list is not None:
self.extract_data_list(extract_list, res.text)
# 处理断言
self.asserts.assert_result(validation, res_json, status_code)
except JSONDecodeError as js:
logs.error('系统异常或接口未请求!')
raise js
except Exception as e:
logs.error(e)
raise e
except Exception as e:
raise e
该方法用于处理 YAML 文件中定义的接口测试用例,包括请求参数构造、动态变量替换、接口调用、响应断言以及数据提取等核心功能。
方法定义
即通知系统,有一个叫做 specification_yaml 的功能,使用时系统需要提供接口的基本信息和测试数据,该功能会完成整个接口测试流程,并通过断言测试是否通过。
def specification_yaml(self, base_info, test_case):
"""
接口请求处理基本方法
:param base_info: yaml文件里面的baseInfo(接口基本信息)
:param test_case: yaml文件里面的testCase(测试用例数据)
:return: 无返回值,但通过断言验证接口行为
"""
步骤详解
1. 定义常量与基础配置
params_type = ['data', 'json', 'params']
url_host = self.conf.get_section_for_data('api_envi', 'host')
params_type:定义请求参数类型白名单,用于分类YAML中不同用途的参数。
url_host:从配置文件中获取当前环境的主机地址
2. 提取接口基本信息并添加 Allure 报告附件
api_name = base_info['api_name']
allure.attach(api_name, f'接口名称:{api_name}', allure.attachment_type.TEXT)
url = url_host + base_info['url']
allure.attach(api_name, f'接口地址:{url}', allure.attachment_type.TEXT)
method = base_info['method']
allure.attach(api_name, f'请求方法:{method}', allure.attachment_type.TEXT)
从 base_info 中提取接口名称、URL 和请求方法。
使用 allure.attach 将这些信息附加到 Allure 报告中,便于测试结果查看。
3. 处理请求头和 Cookie
header = self.replace_load(base_info['header'])
allure.attach(api_name, f'请求头:{header}', allure.attachment_type.TEXT)
cookie = None
if base_info.get('cookies') is not None:
cookie = eval(self.replace_load(base_info['cookies']))
调用 replace_load 对 header 进行变量替换。
如果存在 cookies,则同样进行替换并使用 eval 转换为字典格式。
4. 提取测试用例名称并添加报告附件
case_name = test_case.pop('case_name')
allure.attach(api_name, f'测试用例名称:{case_name}', allure.attachment_type.TEXT)
从 test_case 中提取用例名称并删除原始字段。
添加到 Allure 报告中。
下一次运行操作时,会重新从YAML文件中读取完整全新的数据。

5. 处理断言逻辑
val = self.replace_load(test_case.get('validation'))
test_case['validation'] = val
validation = eval(test_case.pop('validation'))
替换断言表达式中的变量。
用 eval 执行断言表达式,生成实际断言规则。(YAML文件是字符串文本,但测试框架需要python数据结构来执行断言。eval() 的作用就是把字符串文本->python数据结构)
6. 提取数据字段(extract / extract_list)
extract = test_case.pop('extract', None)
extract_list = test_case.pop('extract_list', None)
从测试数据中取出并移除数据提取配置,为后续从接口响应中提取值做准备。
extract:取出单值提取配置。字符串、数字等基本类型。token、ID、状态码等。
extract_list:取出列表值提取配置。列表。商品列表、用户列表等。
取出的数据会送到数据提取执行器,最终存入全局变量池。存入后,其他接口可通过 ${变量名} 直接引用,实现数据跨接口流动。
7. 处理请求参数(data/json/params)
for key, value in test_case.items():
if key in params_type:
test_case[key] = self.replace_load(value)
将测试用例中的变量占位符(如 ${username})替换为变量池里预先存储的实际值,使测试数据可动态配置。
8. 处理文件上传
file, files = test_case.pop('files', None), None
if file is not None:
for fk, fv in file.items():
allure.attach(json.dumps(file), '导入文件')
files = {fk: open(fv, mode='rb')}
如果存在 files 字段,表示是文件上传接口。
使用 open(..., mode='rb') 读取文件内容,并附加到 Allure 报告中。
比如用户上传的头像,产品图片等等,然后将文件上传这种特殊类型的请求数据与普通请求参数区别开,以二进制模式打开文件,准备HTTP请求。并且在Allure测试报告中附加文件信息,确保文件上传功能的测试可追溯和可验证。
9. 发送接口请求
res = self.run.run_main(name=api_name, url=url, case_name=case_name, header=header,
method=method, file=files, cookies=cookie, **test_case)
status_code = res.status_code
allure.attach(self.allure_attach_response(res.json()), '接口响应信息', allure.attachment_type.TEXT)
调用 run_main 方法发送请求。
执行接口测试请求,获取响应结果,并将响应信息记录到Allure测试报告。
10. 处理响应与断言
try:
res_json = json.loads(res.text) # 把json格式转换成字典
if extract is not None:
self.extract_data(extract, res.text)
if extract_list is not None:
self.extract_data_list(extract_list, res.text)
# 处理断言
self.asserts.assert_result(validation, res_json, status_code)
except JSONDecodeError as js:
logs.error('系统异常或接口未请求!')
raise js
except Exception as e:
logs.error(e)
raise e
将接口响应解析为JSON格式
若有 extract 或 extract_list,提取需要的数据并存储到变量池
最后进行断言验证,同时处理可能的解析异常
11. 异常捕获
except Exception as e:
raise e
捕获所有异常,然后重新抛出相同的异常,不做任何处理。
示例说明
假设 YAML 文件如下:
这是登录接口的自动化测试配置,定义了请求参数、响应验证规则和数据提取规则,用于自动化测试登录功能并获取认证token
baseInfo:
api_name: 登录接口 # 接口名称
url: /login # 接口路径
method: POST # 请求方法
header:
Content-Type: application/json # 请求头:JSON格式
cookies: null # Cookie(无)
testCase:
- case_name: 正常登录 # 测试用例名称
json: # 请求体(JSON格式)
username: admin # 用户名
password: ${get_password()} # 密码(动态获取)
validation: # 响应验证规则
code == 200 and msg == "success" # 验证状态码和消息
extract: # 响应数据提取
token: $.data.token # 从响应中提取token值
经过 specification_yaml 处理后:
${get_password()} 会被替换成实际密码。
请求会携带正确的 JSON 数据。
响应中的 token 会被提取保存。
断言会检查是否返回预期结果。
4.断言判断
相等断言模式代码示例
def equal_assert(self, expected_results, actual_results, status_code=None):
"""
相等断言模式
:param expected_results: 预期结果,yaml文件validation值
:param actual_results: 接口实际响应结果
:return:
"""
flag = 0
if isinstance(actual_results, dict) and isinstance(expected_results, dict):
# 找出实际结果与预期结果共同的key
common_keys = list(expected_results.keys() & actual_results.keys())[0]
# 根据相同的key去实际结果中获取,并重新生成一个实际结果的字典
new_actual_results = {common_keys: actual_results[common_keys]}
eq_assert = operator.eq(new_actual_results, expected_results)
if eq_assert:
logs.info(f"相等断言成功:接口实际结果:{new_actual_results},等于预期结果:" + str(expected_results))
allure.attach(f"预期结果:{str(expected_results)}\n实际结果:{new_actual_results}", '相等断言结果:成功',
attachment_type=allure.attachment_type.TEXT)
else:
flag += 1
logs.error(f"相等断言失败:接口实际结果{new_actual_results},不等于预期结果:" + str(expected_results))
allure.attach(f"预期结果:{str(expected_results)}\n实际结果:{new_actual_results}", '相等断言结果:失败',
attachment_type=allure.attachment_type.TEXT)
else:
raise TypeError('相等断言--类型错误,预期结果和接口实际响应结果必须为字典类型!')
return flag
方法说明
方法名称
def equal_assert(self, expected_results, actual_results, status_code=None)
该方法用于实现 相等断言(Equal Assertion),即验证接口返回的实际结果中某个字段的值是否 完全等于预期值。
通过对比两个字典对象(实际结果与预期结果)中的指定字段内容,判断其是否完全一致。
使用场景
验证接口返回的 code 是否为 200
验证 HTTP 状态码是否符合预期
验证响应中的 user.id 是否等于预期值
验证用户名、邮箱等关键信息是否准确
验证对象中的特定属性是否符合要求
返回值说明
返回一个整数类型 flag:
0 表示断言成功 1 表示断言失败
执行逻辑详解
1. 初始化标志位
flag = 0
创建断言结果标志位,默认值 0 表示断言通过状态
用数字标志位便于后续统计失败数量
2. 类型校验
if isinstance(actual_results, dict) and isinstance(expected_results, dict):
检查 actual_results 和 expected_results 是否均为字典类型
是方法执行的前提条件,确保数据格式正确
3. 查找共同字段
common_keys = list(expected_results.keys() & actual_results.keys())[0]
使用集合交集操作找出两个字典中共同的键
通过 & 运算符获取共同键的集合
转换为列表后取第一个元素作为比较目标
4. 构造比较字典
new_actual_results = {common_keys: actual_results[common_keys]}
基于找到的共同字段,从实际结果中提取对应值
创建新的字典,只包含要比较的字段键值对
目的是聚焦比较目标,忽略其他不相关字段
5. 执行相等比较
eq_assert = operator.eq(new_actual_results, expected_results)
用 operator.eq() 函数进行严格的字典相等性比较
比较构建的实际结果字典与预期结果字典是否完全一致
6. 结果处理和记录
if eq_assert:
# 成功分支:记录成功日志和测试报告
logs.info(f"相等断言成功...")
allure.attach(..., '相等断言结果:成功', ...)
else:
# 失败分支:更新标志位并记录错误信息
flag += 1
logs.error(f"相等断言失败...")
allure.attach(..., '相等断言结果:失败', ...)
根据比较结果分别处理成功和失败情况
成功时记录信息级别日志,失败时记录错误级别日志
将详细结果附加到 Allure 测试报告中
7. 异常处理
else:
raise TypeError('相等断言--类型错误...')
当输入参数不是字典类型时抛出类型错误异常
8. 返回结果
return flag
整型标志位:0 表示断言成功,>0 表示断言失败
四.项目结语
本自动化接口测试项目基于Python+Pytest构建了一套完整、高效、可维护的测试解决方案,通过模块化设计和智能化处理,显著提升了接口测试的效率与可靠性。
更多推荐
所有评论(0)