项目概述
这是一个基于Python的接口自动化测试框架,使用了pytest作为测试执行引擎,支持多种报告格式(allure和tmreport),具备完整的测试流程管理能力。
项目结构分析
- 核心目录结构
- base/ : 基础工具模块,包含API请求处理、ID生成等基础功能
- common/ : 公共模块,包括断言、日志记录、数据库连接、邮件发送等通用工具
- conf/ : 配置文件目录,管理环境配置和项目设置
- data/ : 测试数据目录,支持YAML、CSV等多种数据格式
- testcase/ : 测试用例目录,按业务模块组织测试用例
- report/ : 测试报告目录,生成allure和tm两种格式的测试报告
- 核心功能模块
测试执行入口 (run.py)
项目通过run.py文件启动测试,支持两种报告类型:
- allure报告:功能更丰富,支持实时查看
- tmreport报告:简洁的HTML报告格式
配置管理 (conf/)
config.ini: 管理API环境、数据库、Redis、邮件等配置信息setting.py: 项目设置文件operationConfig.py: 配置文件读取工具
测试数据管理 (data/)
支持多种数据格式:
- YAML文件:用于接口测试参数和预期结果
- CSV文件:用于批量数据测试
- XML文件:用于SQL语句管理
测试用例设计 (testcase/)
测试用例按业务模块分类:
- 单接口测试 (Single interface)
- 业务场景测试 (Business interface)
- 产品管理测试 (ProductManager)
核心工具类
-
API 请求处理 (base/apiutil.py)
- 支持参数替换解析(${}表达式)
- 处理文件上传接口
- 支持多种请求方式(GET/POST等)
- 集成allure报告生成
-
断言工具 (common/assertions.py)
- 字符串包含断言
- 相等/不相等断言
- 数据库断言
- 响应时间断言
- 任意值断言
-
数据处理
- YAML数据读取 (common/readyaml.py)
- CSV数据处理 (common/operationcsv.py)
- XML数据处理 (common/operxml.py)
测试流程特点
- 数据驱动: 使用YAML文件管理测试数据和预期结果
- 参数化测试: 通过pytest.mark.parametrize实现测试用例参数化
- 动态报告: 集成allure报告,提供详细的测试执行信息
- 多环境支持: 通过配置文件支持不同测试环境切换
- 数据库验证: 支持数据库断言验证数据一致性
- 日志记录: 完整的测试执行日志记录
测试用例示例
测试用例采用YAML格式定义,包含:
- 接口基本信息(URL、方法、请求头)
- 测试数据
- 预期结果验证规则
- 参数提取规则
这种设计使得测试用例编写简洁明了,非技术人员也能轻松理解和维护测试用例。
整个项目结构清晰,模块划分合理,具备良好的扩展性和维护性,适用于中大型项目的接口自动化测试需求
核心代码
run.py文件是这个自动化测试框架的主程序入口,负责执行测试并生成报告。让我逐行解释其功能:
第1-5行是导入模块部分:
import shutil
import pytest
import os
import webbrowser
from conf.setting import REPORT_TYPE
这里导入了几个关键模块:
shutil:用于文件操作,如复制文件pytest:测试框架核心模块os:操作系统接口模块,用于执行系统命令和路径操作webbrowser:用于打开网页浏览器REPORT_TYPE:从配置文件中导入报告类型设置
第7-19行是主程序逻辑部分:
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__') - 根据配置文件中设置的
REPORT_TYPE值来决定生成哪种类型的测试报告
当 REPORT_TYPE 设置为 'allure' 时:
-
使用
pytest.main()执行测试,参数含义:-s:输出用例打印信息,即不捕获print输出-v:显示详细测试过程--alluredir=./report/temp:指定allure报告数据的存储目录'./testcase':指定测试用例所在的目录--clean-alluredir:在生成新报告前清空allure报告目录--junitxml=./report/results.xml:生成JUnit XML格式的测试结果文件
-
使用
shutil.copy()将环境配置文件复制到报告目录 -
使用
os.system()启动allure服务来展示报告
当 REPORT_TYPE 设置为 'tm' 时:
-
使用
pytest.main()执行测试,参数含义:-vs:输出详细信息和print内容--pytest-tmreport-name=testReport.html:指定生成的tm报告文件名--pytest-tmreport-path=./report/tmreport:指定tm报告存储路径
-
使用
webbrowser.open_new_tab()在新浏览器标签页中打开生成的测试报告
这种设计使得框架可以灵活地支持多种报告格式,用户只需在配置文件中更改 REPORT_TYPE 的值即可切换报告类型。
现在我来详细讲解一个测试用例,我选择 testcase/ProductManager/test_productList.py 中的"获取商品列表"测试用例。
测试用例结构分析
具体是 test_get_product_list 方法:
@allure.story(next(c_id) + "获取商品列表")
@pytest.mark.run(order=1)
@pytest.mark.parametrize('base_info,testcase', get_testcase_yaml('./testcase/ProductManager/getProductList.yaml'))
def test_get_product_list(self, base_info, testcase):
allure.dynamic.title(testcase['case_name'])
RequestBase().specification_yaml(base_info, testcase)
测试数据文件分析
测试数据定义在getProductList.yaml文件中:
- baseInfo:
api_name: 商品列表
url: /coupApply/cms/goodsList
method: Get
header:
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
token: ${get_extract_data(cookie)}
testCase:
- case_name: 获取商品列表
params:
msgType: getHandsetListOfCust
page: 1
size: 20
validation:
- contains: { 'error_code': '0000' }
extract_list:
goodsId: $.goodsList[*].goodsId
测试执行流程
1. 测试装饰器说明
@allure.story:用于在Allure报告中标识测试场景@pytest.mark.run(order=1):指定测试执行顺序为第1个@pytest.mark.parametrize:参数化测试,从YAML文件中读取测试数据
2. 测试执行过程
当执行这个测试用例时,会发生以下步骤:
- 数据加载:通过
get_testcase_yaml()函数读取getProductList.yaml文件中的测试数据 - 动态标题设置:使用
allure.dynamic.title()设置测试用例标题为"获取商品列表" - 接口请求执行:调用
RequestBase().specification_yaml(base_info, testcase)方法处理接口请求
3. 接口请求处理详解
在pythonproject\base\apiutils.py 中
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
0. 参数替换
def get_extract_data(self, node_name, randoms=None) -> str:
"""
获取extract.yaml数据,首先判断randoms是否为数字类型,如果不是就获取下一个node节点的数据
:param node_name: extract.yaml文件中的key值
:param randoms: int类型,
0:随机读取;-1:读取全部,返回字符串形式;-2:读取全部,返回列表形式;其他根据列表索引取值,取第一个值为1,第二个为2,以此类推;
:return:
"""
data = self.read.get_extract_yaml(node_name)
if randoms is not None and bool(re.compile(r'^[-+]?[0-9]*.?[0-9]+([eE][-+]?[0-9]+)?$').match(randoms)):
randoms = int(randoms)
data_value = {
randoms: self.get_extract_order_data(data, randoms),
0: random.choice(data),
-1: ','.join(data),
-2: ','.join(data).split(','),
}
data = data_value[randoms]
else:
data = self.read.get_extract_yaml(node_name, randoms)
return data
处理动态参数 ${get_extract_data(cookie)} ,通过get_extract_data方法从 extract.yaml 文件中获取之前接口保存的cookie值
- 请求构建 :
拼接完整URL:从配置中获取host + YAML中定义的url路径
设置请求方法:GET
设置请求头:包含Content-Type和token
设置请求参数:msgType, page, size
- 发送请求 :
调用 SendRequest().run_main() 发送HTTP请求
- 响应处理 :
@classmethod
def allure_attach_response(cls, response):
if isinstance(response, dict):
allure_response = json.dumps(response, ensure_ascii=False, indent=4)
else:
allure_response = response
return allure_response
使用allure_attach_response方法将响应信息附加到Allure报告中
- 参数提取 :
def extract_data_list(self, testcase_extract_list, response):
"""
提取多个参数,支持正则表达式和json提取,提取结果以列表形式返回
:param testcase_extract_list: yaml文件中的extract_list信息
:param response: 接口的实际返回值,str类型
:return:
"""
try:
for key, value in testcase_extract_list.items():
if "(.+?)" in value or "(.*?)" in value:
ext_list = re.findall(value, response, re.S)
if ext_list:
extract_date = {key: ext_list}
logs.info('正则提取到的参数:%s' % extract_date)
self.read.write_yaml_data(extract_date)
if "$" in value:
# 增加提取判断,有些返回结果为空提取不到,给一个默认值
ext_json = jsonpath.jsonpath(json.loads(response), value)
if ext_json:
extract_date = {key: ext_json}
else:
extract_date = {key: "未提取到数据,该接口返回结果可能为空"}
logs.info('json提取到参数:%s' % extract_date)
self.read.write_yaml_data(extract_date)
except:
logs.error('接口返回值提取异常,请检查yaml文件extract_list表达式是否正确!')
- extract_data_list方法提取响应中的 goodsId 字段,保存到 extract.yaml 文件中供后续接口使用
- 使用JSONPath表达式 $.goodsList[*].goodsId 提取商品列表中的所有商品ID
- 断言验证
def assert_result(self, expected, response, status_code):
"""
断言,通过断言all_flag标记,all_flag==0表示测试通过,否则为失败
:param expected: 预期结果
:param response: 实际响应结果
:param status_code: 响应code码
:return:
"""
all_flag = 0
try:
logs.info("yaml文件预期结果:%s" % expected)
# logs.info("实际结果:%s" % response)
# 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(actual_results=response, expected_results=value)
all_flag = all_flag + flag
elif key == 'db':
flag = self.assert_mysql_data(value)
all_flag = all_flag + flag
else:
logs.error("不支持此种断言方式")
except Exception as exceptions:
logs.error('接口断言异常,请检查yaml预期结果值是否正确填写!')
raise exceptions
if all_flag == 0:
logs.info("测试成功")
assert True
else:
logs.error("测试失败")
assert False
- 调用
assert_result方法进行断言 - 使用 contains 断言模式验证响应中包含 'error_code': '0000' ,表示请求成功
4. 断言机制讲解
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
contains_assert方法中
- 遍历预期结果中的每个键值对
- 如果键是 status_code ,则与响应状态码进行比较
- 其他键通过JSONPath在响应中查找对应值
- 使用 in 操作符检查预期值是否包含在实际值中
- 记录断言结果到日志和Allure报告中
测试用例特点
- 数据驱动:测试数据与测试逻辑分离,便于维护和扩展
- 接口依赖:通过
${get_extract_data()}实现接口间参数传递 - 参数提取:自动提取响应数据供后续接口使用
- 多种断言:支持包含断言、相等断言、数据库断言等多种方式
- 报告集成:与Allure报告深度集成,提供详细的测试信息
- 顺序执行:通过
@pytest.mark.run(order=)控制测试执行顺序
这个测试用例完整地展示了该自动化测试框架的核心功能:数据驱动测试、接口依赖处理、参数提取、断言验证和报告生成。