- 将 `CaseGenerator` 拆分为 `CaseDataLoader`(数据加载)和 `CaseGenerator`(用例构造),实现单一职责原则。 - 引入 `TestTemplateBase` 作为纯净的方法挂载容器,避免逻辑代码污染测试用例。 - 优化 YAML 解析流程,将文件扫描、参数化解析与 pytest 方法构建逻辑完全分离。 - 改进装饰器写法,使用更直观的 @ 语法糖处理 Allure 和 pytest.mark.parametrize。 - 增强执行日志,通过类型注解和实例引用记录更详细的运行上下文。
125 lines
4.3 KiB
Python
125 lines
4.3 KiB
Python
#!/usr/bin/env python
|
||
# coding=utf-8
|
||
|
||
"""
|
||
@author: CNWei,ChenWei
|
||
@Software: PyCharm
|
||
@contact: t6g888@163.com
|
||
@file: creator
|
||
@date: 2026/3/6 10:40
|
||
@desc:
|
||
"""
|
||
import logging
|
||
|
||
import allure
|
||
import pytest
|
||
from pathlib import Path
|
||
from core import settings
|
||
from core.executor import WorkflowExecutor
|
||
from core.session import Session
|
||
from commons.exchange import Exchange
|
||
from commons.file_processors.yaml_processor import YamlProcessor as FileHandle
|
||
from typing import Any, Dict, List, Type, Generator, Tuple
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# 初始化全局组件
|
||
session = Session(settings.base_url)
|
||
exchanger = Exchange(settings.DATA_DIR / "extract.yaml") # 指向 data/extract.yaml
|
||
executor = WorkflowExecutor(session, exchanger)
|
||
|
||
|
||
class TestTemplateBase:
|
||
"""
|
||
具体的测试用例容器。
|
||
此映射类不包含任何逻辑方法,仅用于承载由 Loader 挂载的 test_* 方法。
|
||
"""
|
||
pass
|
||
|
||
|
||
class CaseDataLoader:
|
||
"""
|
||
职责 1: 数据加载器
|
||
负责与底层存储(YAML文件)打交道,输出标准化的原始数据对象
|
||
"""
|
||
|
||
@staticmethod
|
||
def fetch_yaml_cases(cases_dir: str) -> Generator[Tuple[Path, Dict], None, None]:
|
||
"""扫描目录并迭代返回 (文件路径, 原始内容)"""
|
||
yaml_files = Path(cases_dir).glob("**/test_*.yaml")
|
||
for file_path in yaml_files:
|
||
yield file_path, FileHandle(file_path)
|
||
|
||
@staticmethod
|
||
def parse_parametrize(raw_data: Dict, default_name: str) -> Tuple[List[str], List[List[Any]], List[str]]:
|
||
"""解析参数化结构,返回 (字段名列表, 数据值列表, ID列表)"""
|
||
if "parametrize" in raw_data:
|
||
fields = raw_data["parametrize"][0]
|
||
values = raw_data["parametrize"][1:]
|
||
ids = [f"{v[0]}" for v in values]
|
||
else:
|
||
fields = ["case_data"]
|
||
values = [[raw_data]]
|
||
ids = [raw_data.get("title", default_name)]
|
||
return fields, values, ids
|
||
|
||
|
||
class CaseGenerator:
|
||
"""
|
||
职责 2: 用例构造工厂
|
||
负责将数据转化为 pytest 装饰的方法,并挂载到目标类
|
||
"""
|
||
|
||
@classmethod
|
||
def build_and_register(cls, target_cls: Type[TestTemplateBase], cases_dir: str):
|
||
# 1. 通过 Loader 获取数据
|
||
for file_path, raw_data in CaseDataLoader.fetch_yaml_cases(cases_dir):
|
||
# 2. 解析参数化信息
|
||
fields, values, ids = CaseDataLoader.parse_parametrize(raw_data, file_path.stem)
|
||
|
||
# 3. 生成执行函数 (闭包)
|
||
dynamic_test_method = cls._create_case_method(raw_data, fields, values, ids)
|
||
|
||
# 4. 挂载
|
||
method_name = f"test_{file_path.stem}"
|
||
setattr(target_cls, method_name, dynamic_test_method)
|
||
logger.debug(f"Successfully registered: {method_name}")
|
||
|
||
@staticmethod
|
||
def _create_case_method(case_template: Dict, fields: List[str], values: List[Any], ids: List[str]):
|
||
"""封装具体的 pytest 执行节点"""
|
||
|
||
# 预取 Allure 层级信息
|
||
epic = case_template.get("epic", settings.allure_epic)
|
||
feature = case_template.get("feature", settings.allure_feature)
|
||
story = case_template.get("story", settings.allure_story)
|
||
|
||
@allure.epic(epic)
|
||
@allure.feature(feature)
|
||
@allure.story(story)
|
||
@pytest.mark.parametrize("case_args", values, ids=ids)
|
||
def build_actual_case(instance: TestTemplateBase, case_args: List[Any]):
|
||
# 数据组装
|
||
current_params = dict(zip(fields, case_args))
|
||
case_exec_data = {**case_template, **current_params}
|
||
case_title = current_params.get("title", "未命名用例")
|
||
|
||
# 日志记录 (利用 instance 标注来源)
|
||
logger.info(f"🚀 [Runner] Class: {instance.__class__.__name__} | Case: {case_title}")
|
||
|
||
# 执行与断言
|
||
allure.dynamic.title(case_title)
|
||
executor.perform(case_exec_data)
|
||
|
||
# 手动链路装饰 (Allure)
|
||
# run_actual_case = allure.epic(epic)(run_actual_case)
|
||
# run_actual_case = allure.feature(feature)(run_actual_case)
|
||
# run_actual_case = allure.story(story)(run_actual_case)
|
||
|
||
return build_actual_case
|
||
|
||
|
||
if __name__ == '__main__':
|
||
# --- 引导执行 ---
|
||
CaseGenerator.build_and_register(TestTemplateBase, settings.TEST_CASE_DIR)
|