一、项目介绍
本项目基于 Python + pytest + sqlalchemy + requests + allure + jsonpath + yaml + Jenkins + Linux 技术栈,构建了一个接口自动化测试框架,用于测试在线购物商城网站的功能,包括:
- 用户注册
- 用户登录
- 商品上架/下架
- 下单
- 支付等相关功能
二、项目结构说明
以下是项目目录结构:
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 # 主程序入口(执行测试和生成报告)
三、核心代码
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
@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('case_info', get_testcase_yaml('./testcase/Business interface/BusinessScenario.yml'))
- 实现参数化测试,读取 YAML 文件中的测试数据,每组数据触发一次测试。
case_info是每组测试数据的变量名,get_testcase_yaml返回测试用例列表。
示例 YAML 文件
- baseInfo:
api_name: 获取商品列表
method: GET
url: /product/list
- baseInfo:
api_name: 提交订单
method: POST
url: /order/create
测试方法
def test_business_scenario(self, case_info):
case_info是 YAML 文件中的单条测试用例数据。- 方法为每个参数化数据执行一次。
动态设置 Allure 标题
allure.dynamic.title(case_info['baseInfo']['api_name'])
- 使用 YAML 文件中的
api_name设置 Allure 报告的测试用例标题。
执行接口请求
RequestBase().specification_yaml(case_info)
- 调用
RequestBase的specification_yaml方法,发送 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 = 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)
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 文件中的接口测试用例,包括参数构造、变量替换、请求发送、响应断言和数据提取。
步骤
-
基础配置
- 定义请求参数类型:
params_type = ['data', 'json', 'params'] - 获取主机地址:
url_host = self.conf.get_section_for_data('api_envi', 'host')
- 定义请求参数类型:
-
提取接口信息
- 获取接口名称、URL、方法、请求头,附加到 Allure 报告。
-
处理 Cookie
- 如果存在 cookies,进行变量替换并转换为字典。
-
提取测试用例名称
- 从
test_case提取case_name并附加到报告。
- 从
-
处理断言
- 替换断言表达式中的变量,转换为可执行规则。
-
处理请求参数
- 对
data、json、params类型参数进行变量替换。
- 对
-
处理文件上传
- 如果存在
files,读取文件并附加到报告。
- 如果存在
-
发送请求
- 调用
run_main发送请求,记录状态码和响应。
- 调用
-
处理响应与断言
- 转换响应为 JSON,提取数据,执行断言。
-
异常处理
- 捕获并记录异常,确保测试框架识别失败。
示例 YAML
baseInfo:
api_name: 登录接口
url: /login
method: POST
header:
Content-Type: application/json
cookies: null
testCase:
- case_name: 正常登录
json:
username: admin
password: ${get_password()}
validation:
code == 200 and msg == "success"
extract:
token: $.data.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):
common_keys = list(expected_results.keys() & actual_results.keys())[0]
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
说明
-
功能:验证实际结果的字段值是否完全等于预期值。
-
场景:检查
code是否为200,或某个字段是否匹配预期。 -
逻辑:
- 提取公共 key,构造新的实际结果字典。
- 使用
operator.eq进行深度比较。 - 记录成功/失败日志并附加到 Allure 报告。
包含断言模式
def contains_assert(self, value, response, status_code):
"""
字符串包含断言模式,断言预期结果的字符串是否包含在接口的响应信息中
:param value: 预期结果,yaml文件的预期结果值
:param response: 接口实际响应结果
:param status_code: 响应状态码
:return: 返回结果的状态标识
"""
flag = 0
for assert_key, assert_value in value.items():
if assert_key == "status_code":
if assert_value != status_code:
flag += 1
allure.attach(f"预期结果:{assert_value}\n实际结果:{status_code}", '响应代码断言结果:失败',
attachment_type=allure.attachment_type.TEXT)
logs.error("contains断言失败:接口返回码【%s】不等于【%s】" % (status_code, assert_value))
else:
resp_list = jsonpath.jsonpath(response, "$..%s" % assert_key)
if isinstance(resp_list[0], str):
resp_list = ''.join(resp_list)
if resp_list:
assert_value = None if assert_value.upper() == 'NONE' else assert_value
if assert_value in resp_list:
logs.info("字符串包含断言成功:预期结果【%s】,实际结果【%s】" % (assert_value, resp_list))
else:
flag = flag + 1
allure.attach(f"预期结果:{assert_value}\n实际结果:{resp_list}", '响应文本断言结果:失败',
attachment_type=allure.attachment_type.TEXT)
logs.error("响应文本断言失败:预期结果为【%s】,实际结果为【%s】" % (assert_value, resp_list))
return flag
说明
-
功能:验证响应中是否包含预期值,支持状态码和 JSON 字段检查。
-
场景:验证
msg是否包含"登录成功",或状态码是否为200。 -
逻辑:
- 遍历断言规则,分别处理状态码和响应内容。
- 使用
jsonpath提取字段值,判断是否包含预期值。 - 记录日志并附加到 Allure 报告。
四、项目结语
本项目基于 pytest 和 Allure 构建了一套稳定、可扩展的接口自动化测试框架,适用于电商系统业务流程测试。通过数据驱动、变量替换、断言机制和报告可视化,显著提升测试效率与质量。未来将持续优化,增强通用性和智能化水平。