一、项目介绍
基于Python+pytest+sqlalchemy+requests+allure+jsonpath+yaml+Jenkins+Linux
该项目是一个在线购物的商城网站,包括用户注册,登录,下单,上架/下架商品,下单支付等相关功能。
二、项目结构说明
pythonproject-root/ # 项目根目录 ├─ base/ # 基础类封装(核心功能)、测试用例工具 │ └─ new_testcase_tools.py # 测试用例工具类 ├─ common/ # 公共方法封装 ├─ conf/ # 存放全局配置文件目录 │ ├─ config.ini # 环境配置文件 ├─ data/ # 存放测试数据路径 ├─ logs/ # 存放测试日志目录 │ └─ test.20251009.log # 按日期命名的日志文件 ├─ report/ # 测试报告生成目录,目前支持生成两种形式的报告 │ ├─ allureReport # Allure交互式报告 │ └─ tmreport # TMReport表格报告 ├─ testcase/ #存放测试用例文件目录 ├─ venv/ # 本框架使用的虚拟环境 ├─ conftest.py # 全局操作(固定名称) ├─ environment.xml # allure测试报告总览-环境显示内容 ├─ extract.yaml # 接口依赖参数存放文件 ├─ pytest.ini # pytest框架规范约束 ├─ requirements.txt # 本框架所使用的到的第三方库 └─ run.py # 主程序入口三、核心代码
1. 程序入口 run.py
代码如下:
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')代码说明:
主程序入口
if __name__ == '__main__':
- Python 标准写法,表示当脚本作为主程序运行时才执行以下代码块。
- 防止模块被其他脚本导入时意外执行测试逻辑。
Allure报告处理逻辑
if REPORT_TYPE == 'allure': pytest.main( ['-s', '-v', '--alluredir=./report/temp', './testcase', '--clean-alluredir', '--junitxml=./report/results.xml'])参数说明:
参数 含义
-s 输出所有打印信息(不屏蔽 stdout) -v 显示详细测试结果 --alluredir=./report/temp 将 Allure 报告数据保存到指定目录 ./testcase 指定测试用例所在目录 --clean-alluredir 在每次运行前清空之前的报告数据 --junitxml=./report/results.xml 生成 JUnit XML 格式的测试结果文件 shutil.copy('./environment.xml', './report/temp')
复制环境信息文件
environment.xml到报告目录中。Allure 报告会读取此文件并显示当前测试环境信息。
os.system(f'allure serve ./report/temp')
使用 Allure CLI 命令启动本地服务器并展示生成的报告页面。
会在默认浏览器中自动打开报告。
2.测试用例 testcase
商务管理代码示例:
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'))作用:
实现参数化测试,即一个测试方法可以运行多组不同的输入数据。
每一组
case_info数据都会触发一次完整的测试执行。参数说明:
'case_info':表示每组测试数据的变量名,在测试函数中作为参数使用。
get_testcase_yaml(...):调用函数读取指定路径下的 YAML 文件内容,返回一个包含多个测试用例的列表。YAML文件内容
- baseInfo: api_name: 商品详情 url: /coupApply/cms/productDetail method: post header: Content-Type: application/json;charset=UTF-8
测试方法定义
def test_business_scenario(self, case_info):
这是一个pytest 测试方法,每个参数化的
case_info都会触发一次该方法的执行。
self表示这是类中的一个实例方法(属于TestEBusinessScenario类)。
case_info是从 YAML 文件中加载的一条测试用例的数据字典。
动态设置 Allure 报告标题
allure.dynamic.title(case_info['baseInfo']['api_name'])作用:
在生成的 Allure 报告中,为当前测试用例设置一个可读性更强的标题。
标题内容来自于 YAML 文件中
baseInfo.api_name字段。示例效果:
如果
api_name是"获取商品列表",那么在报告中显示的用例名称就是这个值。
执行接口请求和校验
RequestBase().specification_yaml(case_info)作用:
创建
RequestBase实例,并调用其specification_yaml()方法。该方法接收当前的测试用例数据
case_info,并根据其中的配置(如 URL、方法、请求头、预期结果等)发送 HTTP 请求。同时会进行响应断言、日志记录等操作,完成整个接口测试流程。
3.接口测试specification_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 especification_yaml方法解析
该方法用于处理 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:表示请求参数可能包含的类型字段。
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 报告中。
5. 处理断言逻辑
val = self.replace_load(test_case.get('validation')) test_case['validation'] = val validation = eval(test_case.pop('validation'))
替换断言表达式中的变量。
使用
eval执行断言表达式,生成实际断言规则。
6. 提取数据字段(extract / extract_list)
extract = test_case.pop('extract', None) extract_list = test_case.pop('extract_list', None)
从
test_case中提取需要提取的字段名,供后续从响应中提取数据。
7. 处理请求参数(data/json/params)
for key, value in test_case.items(): if key in params_type: test_case[key] = self.replace_load(value)
遍历所有测试用例参数,若为
data、json或params类型,则对其值进行变量替换。
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 报告中。
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
捕获所有异常并重新抛出,确保测试框架可以正确识别失败情况。
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验证响应中的
user.id是否等于预期值验证嵌套结构中的某个字段是否完全匹配预期值
参数说明
| 参数名 | 类型 | 描述 |
| expected_results | dict | 预期结果,通常来自 YAML 文件中的 |
| actual_results | dict | 接口实际返回的 JSON 响应体 |
| statuc_code | int(可选) | HTTP 状态码(当前未使用,且参数名存在拼写错误) |
返回值说明
返回一个整数类型
flag:0表示断言成功1表示断言失败
执行逻辑详解
1. 初始化标志位
flag = 0默认表示所有断言通过。
2. 类型校验
if isinstance(actual_results, dict) and isinstance(expected_results, dict):
如果传入的
actual_results和expected_results不是字典类型,则抛出异常:raise TypeError('相等断言--类型错误,预期结果和接口实际响应结果必须为字典类型!')
3. 获取公共 key(要比较的字段)
common_keys = list(expected_results.keys() & actual_results.keys())[0]
使用集合运算符
&找出两个字典共有的 key。取第一个作为比较字段(⚠️ 当前仅支持单字段比较)。
4. 构造新的实际结果字典
new_actual_results = {common_keys: actual_results[common_keys]}
从实际响应中提取对应字段的值,构造一个新的字典。
5. 使用 operator.eq 进行深度比较
eq_assert = operator.eq(new_actual_results, expected_results)
operator.eq()是 Python 的深度比较函数,可以比较复杂数据结构(如嵌套字典、列表等)是否完全相同。相比
==更加严格和可靠。
6. 成功/失败处理
成功时:
logs.info("相等断言成功...") allure.attach(...)
记录日志并附加到 Allure 报告中。
失败时:
flag += 1 logs.error("相等断言失败...") allure.attach(...)
标记失败,记录错误日志,并将断言信息附加到报告中
四.测试报告
Allure报告部分截图
测试套分布
测试涵盖了三个主要的测试套,分别是testcase.Single interface(单接口测试套)、testcase.Business interface(业务接口测试套)和testcase.ProductManager(产品管理测试套)。各测试套的用例均全部通过,说明不同类型的接口及业务场景相关功能都能正常工作。
总结
本次基于 Allure 生成的测试报告,从多方面展示了测试成果。100% 的通过率以及各测试场景、接口的良好表现可以充分说明所测试系统的功能稳定和接口健壮。同时,Allure 报告清晰的结构和丰富的信息也为后续测试分析、问题定位及项目质量评估提供了有力支持。