diff --git a/core/base_api.py b/core/base_api.py index 31b7895..410c365 100644 --- a/core/base_api.py +++ b/core/base_api.py @@ -6,7 +6,17 @@ from core.session import Session from core import settings class BaseApi: + """ + 所有 API 类的基类。 + 提供基础的 Session 管理和日志记录功能,供具体的业务 API 类继承。 + """ def __init__(self, session: Session = None): + """ + 初始化 BaseApi。 + + Args: + session: HTTP 会话对象。如果未提供,将使用默认配置创建一个新的 Session。 + """ self.session = session or Session(base_url=settings.base_url) self.logger = logging.getLogger(self.__class__.__name__) diff --git a/core/context.py b/core/context.py index 1998455..3eea190 100644 --- a/core/context.py +++ b/core/context.py @@ -22,6 +22,12 @@ class VariableStore: """内存变量仓库:负责 L2 缓存与磁盘的唯一交互""" def __init__(self, seed_file: Path): + """ + 初始化变量仓库。 + + Args: + seed_file: 初始变量文件路径(YAML格式),用于加载种子数据。 + """ self.seed_file = seed_file self.processor = YamlProcessor(seed_file) # 启动时仅加载一次 diff --git a/core/creator.py b/core/creator.py index 78c66b4..a7a4f71 100644 --- a/core/creator.py +++ b/core/creator.py @@ -133,6 +133,15 @@ class CaseGenerator: @classmethod def build_and_register(cls, target_cls: Type[TestTemplateBase], cases_dir: Union[str, Path]): + """ + 构建测试用例并注册到目标测试类中。 + + 遍历指定目录下的用例文件,解析数据,生成测试方法并动态绑定到 target_cls 上。 + + Args: + target_cls: 目标测试类(通常继承自 TestTemplateBase)。 + cases_dir: 测试用例文件所在的目录路径。 + """ # 1. 通过 Loader 获取数据 all_cases = CaseDataLoader.get_all_cases(cases_dir) for index, case_info in enumerate(all_cases): @@ -149,7 +158,18 @@ class CaseGenerator: @staticmethod def _create_case_method(title, entity: CaseEntity): - """封装具体的 pytest 执行节点""" + """ + 封装具体的 pytest 执行节点。 + + 创建并返回一个闭包函数,该函数包含完整的测试执行逻辑(Allure 设置、日志、执行器调用)。 + + Args: + title: 测试用例标题。 + entity: 包含用例数据和上下文的实体对象。 + + Returns: + function: 可被 pytest 识别和执行的测试方法。 + """ case_template = entity.step_data context = entity.row_context def build_actual_case(instance: TestTemplateBase, execution_context): diff --git a/core/exchange.py b/core/exchange.py index d2de678..0a41e5f 100644 --- a/core/exchange.py +++ b/core/exchange.py @@ -24,17 +24,31 @@ T = TypeVar("T", bound=Union[dict, list, str, Any]) class Exchange: + """ + 变量交换器类。 + 负责管理全局变量缓存,核心职能包括: + 1. Extract: 从响应结果中提取变量。 + 2. Replace: 将数据中的变量占位符替换为实际值。 + """ def __init__(self, variable_cache: dict[str, Any]): + """ + 初始化交换器。 + + Args: + variable_cache: 初始变量缓存字典(引用传递,修改会影响源数据)。 + """ self._cache = variable_cache # 匹配标准变量 ${var},排除函数调用 ${func()} self.var_only_pattern = re.compile(r"^\$\{([a-zA-Z_]\w*)}$") @property def global_vars(self) -> dict: + """获取当前全局变量缓存。""" return self._cache @global_vars.setter def global_vars(self, global_vars: dict) -> None: + """设置全局变量缓存(通常用于上下文切换,如 ChainMap 合并)。""" self._cache = global_vars def extract(self, resp: Any, var_name: str, attr: str, expr: str, index: int = 0): diff --git a/core/executor.py b/core/executor.py index 8aafc26..446227c 100644 --- a/core/executor.py +++ b/core/executor.py @@ -25,6 +25,13 @@ VALIDATE_LIST_ADAPTER = TypeAdapter(List[ValidateItem]) class WorkflowExecutor: + """ + 工作流执行器。 + 作为测试执行的核心引擎,负责调度单个用例的完整生命周期: + 1. 上下文准备(变量池合并)。 + 2. 动作路由与执行(HTTP 请求或 PO 方法反射调用)。 + 3. 后处理(变量提取与断言校验)。 + """ @classmethod def perform(cls, case_info: RawSchema, env: ExecutionEnv, context: Optional[dict[str, Any]] = None) -> Any: @@ -74,7 +81,7 @@ class WorkflowExecutor: @staticmethod def _execute_po_method(action: ApiActionModel, env: ExecutionEnv): """核心反射逻辑:根据字符串动态加载 api/ 目录下的类并执行方法""" - class_name = action.api_class + class_name = action.module method_name = action.method params = action.params or {} # 1. 确定模块路径:优先级策略 diff --git a/core/models.py b/core/models.py index cc3d376..65b8884 100644 --- a/core/models.py +++ b/core/models.py @@ -18,6 +18,10 @@ logger = logging.getLogger(__name__) class HttpAction(BaseModel): + """ + HTTP 请求动作模型。 + 定义了发起 HTTP 请求所需的所有参数,包括方法、URL、头信息、参数、请求体等。 + """ method: str = Field(..., description="HTTP 请求方法: get, post, etc.") url: str = Field(..., description="接口路径或完整 URL") headers: dict[str, Any] | None = Field(default=None, description="HTTP 请求头") @@ -31,6 +35,10 @@ class HttpAction(BaseModel): class ApiActionModel(BaseModel): + """ + PO (Page Object) 模式动作模型。 + 定义了调用封装在 API 类中的方法所需的信息,通过反射机制动态执行。 + """ module: str = Field(..., alias="class", description="要调用的 API 类名") method: str = Field(..., description="类中的方法名") params: dict[str, Any] = Field(default_factory=dict, description="传给方法的参数") @@ -39,6 +47,10 @@ class ApiActionModel(BaseModel): class ValidateItem(BaseModel): + """ + 断言项模型。 + 定义了测试用例执行后的校验规则,包括检查字段、断言方法和期望值。 + """ check: str = Field(..., description="要检查的字段或表达式") assert_method: str = Field(alias="assert", default="equals") expect: Any = Field(..., description="期望值") @@ -48,6 +60,10 @@ class ValidateItem(BaseModel): class RawSchema(BaseModel): + """ + 测试用例原始数据模型。 + 对应 YAML 用例文件的结构,包含元数据、动作定义、变量提取和断言规则。 + """ title: str = Field(..., description="用例标题") epic: str | None = None feature: str | None = None diff --git a/core/session.py b/core/session.py index 9779eb2..d8969ad 100644 --- a/core/session.py +++ b/core/session.py @@ -6,7 +6,7 @@ @Software: PyCharm @contact: t6i888@163.com @file: session.py -@date: 2024 2024/9/12 21:56 +@date: 2024/9/12 21:56 @desc: """ import logging @@ -19,22 +19,69 @@ import allure # logger = logging.getLogger("requests.session") logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) +# logging.basicConfig(level=logging.INFO) class Session(requests.Session): + """ + 自定义会话管理类,继承自 requests.Session。 + + 增强功能: + 1. Base URL 管理:支持相对路径自动拼接。 + 2. Allure 集成:自动将请求操作包装为 Allure 步骤。 + 3. 日志记录:详细记录请求和响应的 头部、正文、状态码等信息。 + """ + def __init__(self, base_url=None): + """ + 初始化会话。 + + Args: + base_url: 基础 URL,用于拼接相对路径请求。 + """ super().__init__() # 先执行父类的初始化 self.base_url = base_url # 在执行子类的初始化操作 @allure.step("发送请求") def request(self, method, url: str, *args, **kwargs) -> Response: + """ + 发送 HTTP 请求(重写)。 + + 逻辑: + 1. 如果 url 是相对路径,自动拼接 base_url。 + 2. 记录 Allure 步骤。 + + Args: + method: 请求方法 (GET, POST, etc.) + url: 请求 URL (支持相对路径) + *args: 透传给 requests.Session.request 的位置参数 + **kwargs: 透传给 requests.Session.request 的关键字参数 + + Returns: + Response: 响应对象 + """ if not url.startswith("http"): # 自动添加baseurl url = urljoin(self.base_url, url) return super().request(method, url, *args, **kwargs) # 按照原有方式执行 def send(self, request: PreparedRequest, *args, **kwargs) -> Response: + """ + 发送底层 PreparedRequest(重写)。 + + 逻辑: + 1. 记录请求详细日志 (URL, Headers, Body)。 + 2. 执行真实网络请求。 + 3. 记录响应详细日志 (Status, Headers, Body)。 + + Args: + request: 已准备好的请求对象 + *args: 透传参数 + **kwargs: 透传参数 + + Returns: + Response: 响应对象 + """ logger.info(f"发送请求>>>>>> 接口地址 = {request.method} {request.url}") logger.info(f"发送请求>>>>>> 请求头 = {request.headers}") logger.info(f"发送请求>>>>>> 请求正文 = {request.body} ") diff --git a/docs/架构改进.md b/docs/架构改进.md new file mode 100644 index 0000000..6a3f541 --- /dev/null +++ b/docs/架构改进.md @@ -0,0 +1,80 @@ +# 自动化测试框架架构改进建议 + +本文档基于对当前 `InterfaceAutoTest` 项目代码的深度分析,整理了针对框架稳定性、扩展性和易用性的架构改进建议。 + +## 1. 并发执行支持 (Concurrency Support) + +### 现状问题 +当前 `VariableStore` 使用简单的文件读写 (`extract.yaml`) 来存储全局变量。 +- 在使用 `pytest-xdist` 进行多进程并发测试时,每个进程会加载独立的内存变量副本。 +- 测试结束写回文件时,不同进程会相互覆盖,导致变量提取丢失或数据不一致。 + +### 改进方案 +1. **引入分布式缓存 (推荐)**: + - 使用 **Redis** 作为变量存储后端。 + - Redis 天然支持原子操作和并发读写,能完美解决多进程数据共享问题。 +2. **文件锁机制 (轻量级)**: + - 如果不引入 Redis,需在 `VariableStore` 的读写操作中增加 **文件锁 (File Lock)** (如使用 `filelock` 库)。 + - 这会降低并发性能,但能保证数据一致性。 + +## 2. 配置管理增强 (Configuration Management) + +### 现状问题 +`settings.py` 中存在大量硬编码配置(如 API 映射、日志路径),且缺乏对多环境(Dev/Test/Prod)的动态切换支持。 + +### 改进方案 +1. **多环境配置文件**: + - 建立 `config/` 目录,分离 `base_config.yaml`, `dev.yaml`, `prod.yaml`。 + - 运行时通过环境变量 `ENV=prod` 加载对应配置并合并。 +2. **环境变量集成**: + - 使用 `.env` 文件管理敏感信息和基础路径。 + - 利用 `python-dotenv` 在项目启动时加载环境变量。 + +## 3. 扩展性与钩子机制 (Extensibility & Hooks) + +### 现状问题 +`WorkflowExecutor` 的执行逻辑(准备 -> 请求 -> 后处理)是固定的。如果需要添加自定义逻辑(如请求签名加密、复杂的响应解密),目前很难插入。 + +### 改进方案 +在执行器中引入 **Hooks (钩子)** 机制,允许注册回调函数: +- `before_request(request_data)`: 请求发出前调用,用于修改 Header、计算签名。 +- `after_response(response)`: 收到响应后调用,用于全局解密、统一错误码判断。 +- `before_case(context)` / `after_case(result)`: 用例级别的 setup/teardown。 + +## 4. 安全性管理 (Security) + +### 现状问题 +敏感数据(如密码、SecretKey)可能明文写在 YAML 用例中。 + +### 改进方案 +扩展 `Exchange` 类的变量替换逻辑,增加对环境变量的读取支持: +- **语法示例**: `password: ${ENV:DB_PASSWORD}` +- 在运行时从系统环境变量中读取,避免将其提交到代码仓库。 + +## 5. 可观测性增强 (Observability) + +### 现状问题 +虽然 `Session` 类中有日志记录,但在高并发或海量日志场景下,难以串联单个用例的完整执行链路。 + +### 改进方案 +1. **全链路 Trace ID**: + - 在用例开始执行时生成唯一的 `trace_id`。 + - 将其注入到 `logging` 的 `Extra` 信息中,使其出现在每一行日志里。 + - 同时将 `trace_id` 添加到 HTTP 请求头中(如 `X-Trace-Id`),便于服务端排查。 +2. **结构化日志**: + - 考虑使用 JSON 格式输出日志,便于接入 ELK 等日志分析系统。 + +## 6. 代码健壮性 (Robustness) + +### 修复建议 +- **属性一致性**: 检查 `core/executor.py` 中的 PO 模式反射逻辑,确保属性访问与 `core/models.py` 定义一致。 + - `ApiActionModel` 定义了 `module` (alias=`class`)。 + - 确保执行器中使用 `action.module` 而非 `action.api_class`,防止 `AttributeError`。 + +--- + +**实施路线图建议**: +1. 优先修复代码健壮性问题(属性一致性)。 +2. 实施配置管理增强,便于环境隔离。 +3. 引入 Redis 或文件锁解决并发问题。 +4. 逐步完善 Hooks 和 Trace ID。 \ No newline at end of file diff --git a/docs/重构总结.md b/docs/重构总结.md new file mode 100644 index 0000000..7242850 --- /dev/null +++ b/docs/重构总结.md @@ -0,0 +1,22 @@ +本次重构核心总结:升级为“模型驱动+混合模式”的自动化测试框架我们本次重构的目标是将现有框架从基于字典(dict)的松散操作,升级为一个结构严谨、易于扩展的现代化测试框架。其核心包含以下四大支柱:1. +核心驱动力:Pydantic 模型层•目标:用强类型、带校验的模型对象取代脆弱的字典操作。•实现:创建 commons/models/case_model.py +文件,并定义 CaseInfo 类。•关键收益:•健壮性:在执行测试前,通过模型实例化,对 YAML +文件中的字段、类型、结构进行严格校验,提前发现拼写错误或格式问题。•可维护性:代码中不再出现 case.get("request") +这类“魔法字符串”,而是通过 case.request 这样的属性访问,IDE 可以提供智能提示和补全,代码更清晰、更安全。•灵活性:支持使用 +alias,让 YAML 中的字段名(如 validate)与模型属性名(如 validate_data)解耦,使模型设计更符合 Python 规范。2. +执行模式:支持混合模式(Hybrid Mode)•目标:让框架同时适应简单的数据驱动测试和复杂的业务流测试。•实现:•YAML 驱动模式:保留并优化 +TestAPI 类。它负责扫描 tests/features/ 目录下的 test_*.yaml 文件,并动态生成 pytest 用例。此模式非常适合单接口、多场景的数据验证。•手动脚本模式:允许在 +tests/flows/ 目录下直接编写 test_*.py 脚本。开发者可以像写普通 pytest 用例一样,通过导入业务方法来编排复杂的、跨多个接口的业务流程。3. +架构设计:清晰的三层分离•目标:遵循最佳实践,分离关注点,让框架结构清晰,避免混乱。•实现:•数据层 (YAML + Pydantic Model) +:定义测试的输入数据和预期结果(是什么)。•业务层/服务层 (api/*.py):将原始的 HTTP 请求封装成具有业务含义的方法,如 +api.auth.login()。它定义了如何执行具体业务操作(怎么做)。•测试层 (TestAPI 或 test_*.py) +:作为“导演”,负责编排测试流程。它从数据层获取数据,调用业务层的方法执行动作,并进行最终断言(测什么)。4. +上下文与状态:统一的会话与变量池•目标:打通 YAML 驱动和手动脚本之间的数据壁垒,实现真正的端到端流程测试。•实现:•所有测试(无论来源)共享同一个 +core.session.Session 实例,确保 Cookie、Header 等会话状态的连续性。•所有测试共享同一个 commons.exchange.Exchange +实例(变量交换器)。•关键收益:手动脚本(.py)中通过登录获取的 token,可以被无缝地注入到后续的 YAML 用例中;反之,YAML 用例提取的 +ID 也能被后续的 .py 脚本使用。重构后的标准执行流程(以 YAML 为例):1.加载:TestAPI 扫描并加载 test_*.yaml +文件。2.数据驱动:DataDriver 将 YAML 文件内容解析为多个独立的、参数化的测试用例。3.执行:在 pytest 的 test_func 内部: a. +变量替换:exchanger.replace() 将用例中的 ${variable} 替换为实际值。 b. 模型校验:CaseInfo(**replaced_case_data) 将替换后的字典实例化为 +CaseInfo 模型对象,完成数据校验。(这是与旧流程最核心的区别) c. 请求发送:使用模型对象的数据发送请求 session.request(** +case.request.model_dump())。 d. 变量提取:exchanger.extract() 从响应中提取数据,并存入全局变量池。 e. +断言:validator.assert_all(case.validate_data) 使用模型中的断言数据进行校验。 \ No newline at end of file