童音商城自动化测试项目
·
一、项目介绍
该项目基于Python+pytest+sqlalchemy+requests+allure+jsonpath+yaml+Jenkins+Linux
github链接:点击这里
二、项目结构说明
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__':
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('case_info', get_testcase_yaml('./testcase/Business interface/BusinessScenario.yml'))
实现参数化测试,即一个测试方法可以运行多组不同的输入数据,每一组 case_info 数据都会触发一次完整的测试执行。
yaml文件示例
- baseInfo:
api_name: 获取商品列表
method: GET
url: /product/list
- baseInfo:
api_name: 提交订单
method: POST
url: /order/create
测试方法
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 报告中,为当前测试用例设置一个可读性更强的标题。
执行接口请求和校验
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 e
specification_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)
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)
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')}
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
- 捕获所有异常并重新抛出,确保测试框架可以正确识别失败情况。
示例说明
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
经过 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
- 验证响应中的 user.id 是否等于预期值
- 验证嵌套结构中的某个字段是否完全匹配预期值
参数说明
| 参数名 | 类型 | 描述 |
|---|---|---|
| expected_results | dict | 预期结果,通常来自 YAML 文件中的 validation 字段,例如:{“code”: 200} |
| actual_results | dict | 接口实际返回的 JSON 响应体 |
| status_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(...)
# 失败
flag += 1
logs.error("相等断言失败...")
allure.attach(...)
包含断言模式代码示例
def contains_assert(self, value, response, status_code):
"""
字符串包含断言模式,断言预期结果的字符串是否包含在接口的响应信息中
:param value: 预期结果,yaml文件的预期结果值
:param response: 接口实际响应结果
:param status_code: 响应状态码
:return: 返回结果的状态标识
"""
# 断言状态标识,0成功,其他失败
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
方法说明
方法名称
def contains_assert(self, value, response, status_code)
该方法用于实现 字符串包含断言(Contains Assertion),即验证接口响应中是否 包含预期的关键字段和值。支持对状态码和 JSON 响应内容进行断言。
示例场景
- 验证返回的 msg 是否包含 “登录成功”
- 验证 HTTP 状态码是否为 200
- 验证响应中的某个字段是否包含指定值
参数说明
| 参数名 | 类型 | 描述 |
|---|---|---|
| value | dict | 从 YAML 文件中读取的断言规则,例如 { “code”: 200, “msg”: “登录成功” } |
| response | dict | 接口实际返回的 JSON 响应体 |
| status_code | int | 接口返回的 HTTP 状态码 |
- 返回一个整数类型 flag:0表示所有断言通过,大于0表示有断言失败,数值代表失败次数
执行逻辑详解
1. 初始化标志位
flag = 0
默认表示所有断言通过。
2. 遍历断言字典
for assert_key, assert_value in value.items():
遍历传入的 value 字典,获取每个断言字段及其预期值。
3. 判断是否为状态码断言
if assert_key == "status_code":
if assert_value != status_code:
flag += 1
allure.attach(...) # 添加失败信息到 Allure 报告
logs.error(...) # 记录错误日志
- 如果是 “status_code”,则比较预期值与实际状态码。
- 不相等时标记失败,并记录日志和报告信息。
4. 否则进行响应内容断言(JSONPath 提取 + 包含判断)
resp_list = jsonpath.jsonpath(response, "$..%s" % assert_key)
使用 jsonpath.jsonpath() 从响应数据中提取所有名为 assert_key 的字段值。
response = {"code": 200, "data": {"msg": "登录成功"}}
assert_key = "msg"
resp_list = jsonpath.jsonpath(response, "$..msg") → ["登录成功"]
对结果做类型处理
if isinstance(resp_list[0], str):
resp_list = ''.join(resp_list)
- 如果提取到的是字符串列表,合并成单个字符串方便后续判断。
判断是否为空值(如 NONE)
assert_value = None if assert_value.upper() == 'NONE' else assert_value
- 支持将字符串 ‘NONE’ 转换为 Python 的 None,便于后续判断空值。
实际断言逻辑
if assert_value in resp_list:
logs.info("成功")
else:
flag += 1
allure.attach(...)
logs.error("失败")
更多推荐



所有评论(0)