Files
apiautotest/utils/case_generate_utils/case_fun_generate.py

304 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 loguru import logger
# 本地应用/模块导入
from common.files_utils.files_handle import load_yaml_file, get_files, get_relative_path
from settings import CASE_FILE_TYPE, CUSTOM_MARKERS, AUTO_CASE_DIR, INTERFACE_DIR
from utils.case_generate_utils.case_data_analysis import CaseDataCheck
"""
主要步骤:
1. 从用例数据文件EXCEL/YAML中获取用例数据
2. 分析用例数据是否符合规范
3. 确认符合规范后获取所有用例数据自动生成测试用例方法PY
"""
CASE_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "case_template.txt")
CONFTEST_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "conftest_template.txt")
def __load_yaml_data(file):
"""
读取yaml用例数据生成测试用例
"""
if os.path.isfile(file):
# 读取yaml/yml文件中的用例数据存储到data中
yaml_data = load_yaml_file(file)
logger.debug(f"需要处理的文件:{file}")
if os.path.samefile(INTERFACE_DIR, os.path.dirname(file)):
"""# os.path.samefile是 Python 中 os.path 模块提供的一个函数,用于检查两个文件路径是否指向同一个文件。"""
if os.path.basename(file) == "init_data.yaml" or os.path.basename(file) == "init_data.yml":
"""识别到init_data.yaml或者init_data.yml文件自动生成conftest.py文件"""
os.makedirs(os.path.dirname(file), exist_ok=True)
logger.debug(f"识别到init_data.yaml或者init_data.yml文件自动生成conftest.py文件")
generate_conftest_file(
template_path=CONFTEST_TEMPLATE_DIR,
init_data=yaml_data,
target_path=AUTO_CASE_DIR
)
elif os.path.basename(file).startswith("test"):
# 检查用例数据是否符合规范
tested_case = CaseDataCheck().case_process(yaml_data)
gen_case_file(
# 用例文件的直接父级目录是INTERFACE_DIR则直接在AUTO_CASE_DIR下生成测试用例方法
filename=os.path.splitext(os.path.basename(file))[0],
case_template_path=CASE_TEMPLATE_DIR,
config=yaml_data["config"],
common_dependence=yaml_data.get("common_dependence", None),
case_data=tested_case,
target_case_path=AUTO_CASE_DIR
)
else:
logger.error(f"{file}不是以init_data或者test开头的文件")
else:
# 用例文件的直接父级目录不是INTERFACE_DIR 则保留其直接父级目录再在AUTO_CASE_DIR下生成测试用例方法
if os.path.basename(file) == "init_data.yaml" or os.path.basename(file) == "init_data.yml":
"""识别到init_data.yaml或者init_data.yml文件自动生成conftest.py文件"""
os.makedirs(os.path.dirname(file), exist_ok=True)
logger.debug(f"识别到init_data.yaml或者init_data.yml文件自动生成conftest.py文件")
generate_conftest_file(
template_path=CONFTEST_TEMPLATE_DIR,
init_data=yaml_data,
target_path=os.path.join(AUTO_CASE_DIR,
get_relative_path(file_path=file, directory_path=INTERFACE_DIR))
)
elif os.path.basename(file).startswith("test"):
# 检查用例数据是否符合规范
tested_case = CaseDataCheck().case_process(yaml_data)
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,
config=yaml_data["config"],
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))
)
else:
logger.error(f"{file}不是以init_data或者test开头的文件")
return True
else:
logger.error(f"{file}不是一个正确的文件路径!")
return False
def generate_cases():
"""
根据配置文件,从指定类型文件中读取所有用例数据,并自动生成测试用例
"""
try:
yaml_files = []
if CASE_FILE_TYPE == 1:
# 在用例数据"INTERFACE_DIR"目录中寻找后缀是yaml, yml的文件
yaml_files = get_files(target=INTERFACE_DIR, start="test_", end=".yaml") \
+ get_files(target=INTERFACE_DIR, start="test_", end=".yml") \
+ get_files(target=INTERFACE_DIR, start="init_data", end=".yml") \
+ get_files(target=INTERFACE_DIR, start="init_data", end=".yaml")
else:
logger.error(f"{CASE_FILE_TYPE}不在CaseFileType内不能自动生成用例")
# 自动生成测试用例
for file in yaml_files:
__load_yaml_data(file=file)
except Exception as e:
logger.error(f"生成用例时发生错误: {e}")
def generate_conftest_file(init_data, template_path, target_path):
"""
识别到目录中的init_data.yaml或者init_data.yml文件自动生成conftest.py文件
:param init_data: 需要初始化的数据
:param template_path: 模板的绝对路径
:param target_path: conftest.py的需要生成的目录
"""
try:
# 如果自动生成用例的目录不存在则自动创建一个
"""
exist_ok=True 是一个可选参数,用于指定在目录已经存在的情况下是否忽略错误。
如果设置为 True则不论目录是否已存在os.makedirs 都不会报错;如果设置为 False默认值则在目录已存在时会引发 FileExistsError 异常。
"""
os.makedirs(target_path, exist_ok=True)
# 先读取用例模板中每一行的内容
with open(file=template_path, mode="r", encoding="utf-8") as f:
current_template = ''.join(f.readlines())
# 根据模板,生成测试用例方法
"""
string.Template是将一个string设置为模板通过替换变量的方法最终得到想要的string。
"""
conftest_content = Template(current_template).safe_substitute(
{
"init_data": init_data,
}
)
# 基于模板生成conftest.py文件
filepath = os.path.join(target_path, 'conftest.py')
with open(filepath, "w", encoding="utf-8") as fp:
fp.write(conftest_content)
logger.debug(f"conftest.py文件创建成功:{filepath}")
except Exception as e:
logger.error(f"生成conftest.py文件时发生错误: {e}")
def gen_case_file(filename, case_template_path, config, common_dependence, case_data, target_case_path):
"""
根据测试用例文件(yaml/yml),以及事先定义的测试用例模板,实际用例数据,生成测试用例方法(.py)
:param filename: 测试用例文件(yaml/yml)的名称,用作生成测试用例类名,方法名
:param case_template_path: 测试用例模板的绝对路径
:param config: 用例公共参数
:param common_dependence: 用例公共依赖
:param case_data: 实际用例数据
:param target_case_path: 测试用例方法(.py)的需要生成的目录
"""
try:
# 如果自动生成用例的目录不存在则自动创建一个
if not os.path.exists(target_case_path):
"""
exist_ok=True 是一个可选参数,用于指定在目录已经存在的情况下是否忽略错误。
如果设置为 True则不论目录是否已存在os.makedirs 都不会报错;如果设置为 False默认值则在目录已存在时会引发 FileExistsError 异常。
"""
os.makedirs(target_case_path, exist_ok=True)
# 获取用例数据中的标记
pytest_markers = config.get("pytest_markers", []) or []
# logger.debug(f"从用例中拿到的标记有:{pytest_markers} {type(pytest_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"获取到的pytest_markers{pytest_markers} {type(pytest_markers)}")
for case_marker in pytest_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(
{
"epic": config["epic"],
"feature": config["feature"],
"story": config["story"],
"pytest_markers": pytest_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(
{
"epic": config["epic"],
"feature": config["feature"],
"story": config["story"],
"pytest_markers": pytest_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)
except Exception as e:
logger.error(f"生成测试用例文件时发生错误: {e}")
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