Files
apiautotest/utils/case_generate_utils/case_fun_generate.py

316 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
# @Time : 2023/6/7 10:07
# @Author : chenyinhua
# @File : case_fun_generate.py
# @Software: PyCharm
# @Desc:
# 标准库导入
import os
from string import Template
import datetime
import re
# 第三方库导入
from xpinyin import Pinyin # 纯 Python 编写的中文字符串拼音转换模块,不需要依赖外部程序和词库。
from loguru import logger
# 本地应用/模块导入
from utils.files_utils.excel_handle import ExcelHandle
from utils.files_utils.yaml_handle import YamlHandle
from utils.files_utils.files_handle import get_files, get_relative_path
from utils.models import CaseFileType
from config.settings import CASE_FILE_TYPE
from config.path_config import INTERFACE_DIR, CASE_TEMPLATE_DIR, AUTO_CASE_DIR
from config.global_vars import CUSTOM_MARKERS
from utils.case_generate_utils.case_data_analysis import CaseDataCheck
"""
主要步骤:
1. 从用例数据文件EXCEL/YAML中获取用例数据
2. 分析用例数据是否符合规范
3. 确认符合规范后获取所有用例数据自动生成测试用例方法PY
"""
def handle_excel_data(file):
"""
读取excel用例数据生成测试用例
"""
if os.path.isfile(file):
# 读取excel文件中的用例数据存储到data中
data = ExcelHandle(file).read()
for index, v in enumerate(data):
excel_case = {}
"""
将excel读取到的用例数据适配allure格式
表单名称以短横线隔开的情况下左边部分作为allure_epic 右边部分作为allure_feature以及allure_story。
否则表单名称全部作为allure_epicallure_featureallure_story
"""
if "-" in v["sheet_name"]:
excel_case["case_common"] = {
"allure_epic": v["sheet_name"].split("-")[0],
"allure_feature": v["sheet_name"].split("-")[1],
"allure_story": v["sheet_name"].split("-")[1]
}
else:
excel_case["case_common"] = {
"allure_epic": v["sheet_name"],
"allure_feature": v["sheet_name"],
"allure_story": v["sheet_name"]
}
# 处理excel用例数据
excel_case["case_info"] = v["data"]
# 检查用例数据是否符合规范
tested_case = CaseDataCheck().case_process(excel_case)
# 生成测试方法
"""
由于excel涉及到多个表单每一个表单都会生成一个测试方法。因此会将表单名称的首字母拼接到测试方法上。
excel名称test_demo
例如表单名称:"GitLink-登录模块""登录模块",都是取关键字"登录模块"首字母
测试文件test_demo_dl.py
测试方法名称: TestDemoDl.test_demo_dl
"""
pin_yin = Pinyin()
_name = pin_yin.get_initials(excel_case["case_common"]["allure_feature"], "").lower()
if os.path.samefile(INTERFACE_DIR, os.path.dirname(file)):
# 用例文件的直接父级目录是INTERFACE_DIR则直接在AUTO_CASE_DIR下生成测试用例方法
gen_case_file(
filename=os.path.splitext(os.path.basename(file))[0] + "_" + _name,
case_template_path=CASE_TEMPLATE_DIR,
case_common=excel_case["case_common"],
common_dependence=None,
case_data=tested_case,
target_case_path=AUTO_CASE_DIR
)
else:
# 用例文件的直接父级目录不是INTERFACE_DIR 则保留其直接父级目录再在AUTO_CASE_DIR下生成测试用例方法
gen_case_file(
filename=os.path.splitext(os.path.basename(file))[0] + "_" + _name,
case_template_path=CASE_TEMPLATE_DIR,
case_common=excel_case["case_common"],
common_dependence=None,
case_data=tested_case,
target_case_path=os.path.join(AUTO_CASE_DIR,
get_relative_path(file_path=file, directory_path=INTERFACE_DIR))
)
return True
else:
logger.error(f"{file}不是一个正确的文件路径!")
return False
def handle_yaml_data(file):
"""
读取yaml用例数据生成测试用例
"""
if os.path.isfile(file):
# 读取yaml/yml文件中的用例数据存储到data中
yaml_data = YamlHandle(file).read_yaml
# 检查用例数据是否符合规范
tested_case = CaseDataCheck().case_process(yaml_data)
if os.path.samefile(INTERFACE_DIR, os.path.dirname(file)):
gen_case_file(
# 用例文件的直接父级目录是INTERFACE_DIR则直接在AUTO_CASE_DIR下生成测试用例方法
filename=os.path.splitext(os.path.basename(file))[0],
case_template_path=CASE_TEMPLATE_DIR,
case_common=yaml_data["case_common"],
common_dependence=yaml_data.get("common_dependence", None),
case_data=tested_case,
target_case_path=AUTO_CASE_DIR
)
else:
# 用例文件的直接父级目录不是INTERFACE_DIR 则保留其直接父级目录再在AUTO_CASE_DIR下生成测试用例方法
os.makedirs(os.path.dirname(file), exist_ok=True)
gen_case_file(
filename=os.path.splitext(os.path.basename(file))[0],
case_template_path=CASE_TEMPLATE_DIR,
case_common=yaml_data["case_common"],
common_dependence=yaml_data.get("common_dependence", None),
case_data=tested_case,
target_case_path=os.path.join(AUTO_CASE_DIR,
get_relative_path(file_path=file, directory_path=INTERFACE_DIR))
)
return True
else:
logger.error(f"{file}不是一个正确的文件路径!")
return False
def generate_cases():
"""
根据配置文件,从指定类型文件中读取所有用例数据,并自动生成测试用例
"""
excel_files = []
yaml_files = []
if CASE_FILE_TYPE == CaseFileType.EXCEL.value:
# 在用例数据"INTERFACE_DIR"目录中寻找后缀是xlsx, xls的文件
excel_files = get_files(target=INTERFACE_DIR, start="test_", end=".xlsx") \
+ get_files(target=INTERFACE_DIR, start="test_", end=".xls")
elif CASE_FILE_TYPE == CaseFileType.YAML.value:
# 在用例数据"INTERFACE_DIR"目录中寻找后缀是yaml, yml的文件
yaml_files = get_files(target=INTERFACE_DIR, start="test_", end=".yaml") \
+ get_files(target=INTERFACE_DIR, start="test_", end=".yml")
elif CASE_FILE_TYPE == CaseFileType.ALL.value:
# 在用例数据"INTERFACE_DIR"目录中寻找后缀是xlsx,xls, yaml, yml的文件
excel_files = get_files(target=INTERFACE_DIR, start="test_", end=".xlsx") + get_files(target=INTERFACE_DIR,
start="test_",
end=".xls")
yaml_files = get_files(target=INTERFACE_DIR, start="test_", end=".yaml") + get_files(target=INTERFACE_DIR,
start="test_",
end=".yml")
else:
logger.error(f"{CASE_FILE_TYPE}不在CaseFileType内不能自动生成用例")
# 自动生成测试用例
for file in excel_files:
handle_excel_data(file=file)
for file in yaml_files:
handle_yaml_data(file=file)
def gen_case_file(filename, case_template_path, case_common, common_dependence, case_data, target_case_path):
"""
根据测试用例文件(yaml/yml/xlsx/xls),以及事先定义的测试用例模板,实际用例数据,生成测试用例方法(.py)
:param filename: 测试用例文件(yaml/yml/xlsx/xls)的名称,用作生成测试用例类名,方法名
:param case_template_path: 测试用例模板的绝对路径
:param case_common: 用例公共参数
:param common_dependence: 用例公共依赖
:param case_data: 实际用例数据
:param target_case_path: 测试用例方法(.py)的绝对路径
"""
# 如果自动生成用例的目录不存在则自动创建一个
if not os.path.exists(target_case_path):
"""
exist_ok=True 是一个可选参数,用于指定在目录已经存在的情况下是否忽略错误。
如果设置为 True则不论目录是否已存在os.makedirs 都不会报错;如果设置为 False默认值则在目录已存在时会引发 FileExistsError 异常。
"""
os.makedirs(target_case_path, exist_ok=True)
# 获取用例数据中的标记
case_markers = case_common.get("case_markers", []) or []
# logger.debug(f"从用例中拿到的标记有:{case_markers} {type(case_markers)}")
# 先读取用例模板中每一行的内容
with open(file=case_template_path, mode="r", encoding="utf-8") as f:
case_template = f.readlines()
current_case_template = []
for line_num, content in enumerate(case_template):
current_case_template.append(content)
# 这里是预计往 @pytest.mark.parametrize( 这一行的上面插入标记
if content.strip().startswith('@pytest.mark.parametrize('):
# 往测试用例模板中插入自定义标记
# logger.debug(f"获取到的case_markers{case_markers} {type(case_markers)}")
for case_marker in case_markers:
# 获取符合要求格式的自定义标记名称,并插入到测试模板中
marker = is_valid_marker(case_marker)
if marker and isinstance(marker, str):
# 注意这里的4个空格必须要有4个空格
# current_case_template.append(f" @pytest.mark.{marker}\n")
# 由于在测试用例中移除了测试类,直接使用测试方法,因此此处不需要空格了,可以直接顶格写
current_case_template.append(f"@pytest.mark.{marker}\n")
if marker and isinstance(marker, dict):
for k, v in marker.items():
# 注意这里的4个空格必须要有4个空格
# current_case_template.append(f" @pytest.mark.{k}('{v}')\n")
# 由于在测试用例中移除了测试类,直接使用测试方法,因此此处不需要空格了,可以直接顶格写
current_case_template.append(f"@pytest.mark.{k}('{v}')\n")
# 将用例数据的名称作为测试用例文件名称, 如test_login_demo
func_name = filename
# 方法名test_demo的类名是TestDemo在测试用例中移除了测试类直接使用测试方法所以下行代码注释
# class_name = "".join([word.capitalize() for word in func_name.split("_")])
if common_dependence:
# 如果存在公共依赖,则对公共依赖进行处理: 识别到模板中的关键字“公共依赖”往下一行插入pytest的fixture作用域级别是class
keyword = "公共依赖"
indices = [i for i, x in enumerate(current_case_template) if re.search(keyword, x, re.IGNORECASE)]
common_dependence_template = ['common_dependence = ${common_dependence}\n', '\n', '\n',
'@pytest.fixture(scope="class", autouse=True)\n',
'def background():\n',
' dependence_results = dependence_handler.case_dependence_handle(case_dependence=common_dependence.get("setup"), db_info=GLOBAL_VARS["db_info"])\n',
' GLOBAL_VARS.update(dependence_results if dependence_results else {})\n',
' yield\n',
' dependence_results = dependence_handler.case_dependence_handle(case_dependence=common_dependence.get("teardown"), db_info=GLOBAL_VARS["db_info"])\n',
' GLOBAL_VARS.update(dependence_results if dependence_results else {})\n',
'\n', '\n']
for idx in reversed(indices):
current_case_template[idx + 1:idx + 1] = common_dependence_template
# 根据模板,生成测试用例方法
"""
string.Template是将一个string设置为模板通过替换变量的方法最终得到想要的string。
"""
current_template = ''.join(current_case_template)
my_case = Template(current_template).safe_substitute(
{
"allure_epic": case_common["allure_epic"],
"allure_feature": case_common["allure_feature"],
"allure_story": case_common["allure_story"],
"case_markers": case_markers,
"common_dependence": common_dependence,
"case_data": case_data,
"func_title": func_name,
# 在测试用例中移除了测试类,直接使用测试方法
# "class_title": class_name,
"now": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
)
else:
# 根据模板,生成测试用例方法
"""
string.Template是将一个string设置为模板通过替换变量的方法最终得到想要的string。
"""
current_template = ''.join(current_case_template)
my_case = Template(current_template).safe_substitute(
{
"allure_epic": case_common["allure_epic"],
"allure_feature": case_common["allure_feature"],
"allure_story": case_common["allure_story"],
"case_markers": case_markers,
"case_data": case_data,
"func_title": func_name,
# 在测试用例中移除了测试类,直接使用测试方法
# "class_title": class_name,
"now": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
)
# 将测试用例方法写入py文件中
with open(os.path.join(target_case_path, func_name + '.py'), "w", encoding="utf-8") as fp:
fp.write(my_case)
def is_valid_marker(markers):
"""
检查标记名称是否合法:仅支持非数字/下划线开头,由数字,字母,下划线组成的标记名称
"""
pattern = r'^[a-zA-Z_][a-zA-Z0-9_]*$'
if isinstance(markers, str):
if re.match(pattern, markers):
# 将自定义标记放到CUSTOM_MARKERS 方便后续统一注册
if markers not in ("skip", "skipif", "parametrize", "usefixtures", "xfail", "filterwarings"):
CUSTOM_MARKERS.append(markers)
# 返回合法有效的标记名称,用于添加到测试方法中
return markers
elif "repeat" in markers:
if re.match(r'repeat\(\d+\)', markers):
return markers
else:
logger.error(f"{markers} 格式不合法, 正确格式参考repeat(2)")
return False
else:
logger.error(f"{markers} 格式不合法, 建议仅输入数字,字母,下划线组合,且不能以数字,下划线开头")
return False
elif isinstance(markers, dict):
if len(markers) == 1:
marker_name = list(markers.keys())[0]
if re.match(pattern, marker_name):
# 将自定义标记放到CUSTOM_MARKERS 方便后续统一注册
if marker_name not in ("skip", "skipif", "parametrize", "usefixtures", "xfail", "filterwarings"):
CUSTOM_MARKERS.append(markers)
return markers
else:
logger.error(f"{markers} 格式不合法, 建议仅输入数字,字母,下划线组合,且不能以数字,下划线开头")
return None
else:
logger.error(f"{markers} 格式不合法, 只能存在一对键值对")
return None
else:
logger.error(f"{markers} 仅支持字符串或者字典格式")
return None