# APITestCase 开发指南 本文档旨在指导开发人员如何创建和使用自定义的 `APITestCase` 类,以扩展 DDMS 合规性验证软件的测试能力。通过继承 `BaseAPITestCase`,您可以编写灵活且强大的 Python 代码来定义针对 API 的各种验证逻辑。 ## 1. `APITestCase` 概述 `APITestCase` 是 DDMS 合规性验证软件中定义具体测试逻辑的核心单元。每个派生自 `BaseAPITestCase` 的类代表一个独立的测试场景或一组相关的检查点,例如验证特定的请求头、检查响应状态码、或确保响应体符合特定业务规则。 **核心理念:** * **代码即测试**:使用 Python 的全部功能来定义复杂的测试逻辑,摆脱传统基于配置或简单规则的限制。 * **灵活性**:允许测试用例在 API 请求的各个阶段介入,包括请求数据的生成、请求发送前的预校验、以及响应接收后的深度验证。 * **可重用性与模块化**:常见的验证逻辑可以封装在辅助函数或基类中,方便在多个测试用例间共享。 在测试执行期间,测试编排器(`APITestOrchestrator`)会自动发现、加载并执行适用于当前 API 端点的所有已注册的 `APITestCase` 实例。 ## 2. 如何创建自定义测试用例 创建一个新的测试用例涉及以下步骤: 1. **创建 Python 文件**:在指定的测试用例目录(例如 `custom_testcases/`)下创建一个新的 `.py` 文件。建议文件名能反映其测试内容,例如 `header_validation_tests.py`。 2. **继承 `BaseAPITestCase`**:在文件中定义一个或多个类,使其继承自 `your_project.test_framework_core.BaseAPITestCase` (请替换为实际路径)。 3. **定义元数据**:在您的自定义测试用例类中,必须定义以下类属性: * `id: str`: 测试用例的全局唯一标识符。建议使用前缀来分类,例如 `"TC-HEADER-001"`。 * `name: str`: 人类可读的测试用例名称,例如 `"必要请求头 X-Tenant-ID 存在性检查"`。 * `description: str`: 对测试用例目的和范围的详细描述。 * `severity: TestSeverity`: 测试用例的严重程度,使用 `TestSeverity` 枚举(例如 `TestSeverity.CRITICAL`, `TestSeverity.HIGH`, `TestSeverity.MEDIUM`, `TestSeverity.LOW`, `TestSeverity.INFO`)。 * `tags: List[str]`: 一个字符串列表,用于对测试用例进行分类和过滤,例如 `["header", "security", "core-functionality"]`。 4. **可选:控制适用范围**:您可以选择性地定义以下类属性来限制测试用例的应用范围: * `applicable_methods: Optional[List[str]]`: 一个 HTTP 方法字符串的列表(大写),例如 `["POST", "PUT"]`。如果定义了此属性,则该测试用例仅应用于具有这些方法的 API 端点。如果为 `None`(默认),则适用于所有方法。 * `applicable_paths_regex: Optional[str]`: 一个 Python 正则表达式字符串。如果定义了此属性,则该测试用例仅应用于其路径与此正则表达式匹配的 API 端点。如果为 `None`(默认),则适用于所有路径。 5. **实现验证逻辑**:重写 `BaseAPITestCase` 中一个或多个 `generate_*` 或 `validate_*` 方法来实现您的具体测试逻辑。 **示例骨架:** ```python # In custom_testcases/my_custom_header_check.py from your_project.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext # 替换为实际路径 import logging # 推荐为每个测试用例获取 logger class MySpecificHeaderCheck(BaseAPITestCase): # 1. 元数据 id = "TC-MYHEADER-001" name = "自定义头部 My-Custom-Header 格式检查" description = "验证请求中 My-Custom-Header 是否存在且格式为 UUID。" severity = TestSeverity.MEDIUM tags = ["custom", "header", "format"] # 2. 可选:适用范围 (例如,仅用于 POST 请求) applicable_methods = ["POST"] # applicable_paths_regex = r"/api/v1/orders/.*" # 示例:仅用于特定路径模式 def __init__(self, endpoint_spec: dict, global_api_spec: dict): super().__init__(endpoint_spec, global_api_spec) # self.logger 在基类中已初始化为 logging.getLogger(f"testcase.{self.id}") self.logger.info(f"测试用例 {self.id} 已针对端点 {self.endpoint_spec.get('path')} 初始化。") # 3. 实现验证逻辑 (见下一节) def generate_headers(self, current_headers: dict) -> dict: # 示例:确保我们的自定义头存在,如果不存在则添加一个用于测试 if "My-Custom-Header" not in current_headers: current_headers["My-Custom-Header"] = "default-test-uuid-value" # 实际应生成有效UUID return current_headers def validate_request_headers(self, headers: dict, request_context: APIRequestContext) -> list[ValidationResult]: results = [] custom_header_value = headers.get("My-Custom-Header") if not custom_header_value: results.append(ValidationResult(passed=False, message="请求头缺少 'My-Custom-Header'。")) else: # 假设有一个 is_valid_uuid 函数 # if not is_valid_uuid(custom_header_value): # results.append(ValidationResult(passed=False, message=f"'My-Custom-Header' 的值 '{custom_header_value}' 不是有效的UUID格式。")) # else: results.append(ValidationResult(passed=True, message="'My-Custom-Header' 存在且格式初步检查通过。")) return results # ... 其他可能需要重写的方法 ... ``` ## 3. `BaseAPITestCase` 详解 `BaseAPITestCase` 提供了一系列可以在子类中重写的方法,这些方法覆盖了 API 测试生命周期的不同阶段。 ### 3.1 构造函数 (`__init__`) ```python def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any]): ``` * 当测试编排器为某个 API 端点实例化您的测试用例时,会调用此构造函数。 * **参数**: * `endpoint_spec: Dict[str, Any]`: 当前正在测试的 API 端点的详细定义。这些信息直接来自 YAPI/Swagger 解析器解析得到的该端点的具体规范,例如包含路径、方法、参数定义(路径参数、查询参数、请求头参数)、请求体 schema、响应 schema 等。您可以使用这些信息来指导您的测试逻辑,例如,了解哪些字段是必需的,它们的数据类型是什么等。 * `global_api_spec: Dict[str, Any]`: 完整的 API 规范文档(例如,整个 YAPI 导出的 JSON 数组或整个 Swagger JSON 对象)。这允许测试用例在需要时访问 API 规范的全局信息,比如全局定义、标签、分类等。 * **注意**: 基类 `__init__` 方法会初始化 `self.endpoint_spec`, `self.global_api_spec` 和 `self.logger`。如果您重写 `__init__`,请务必调用 `super().__init__(endpoint_spec, global_api_spec)`。 ### 3.2 请求生成与修改方法 这些方法在测试编排器构建 API 请求之前被调用,允许您动态地修改或生成请求的各个部分。这对于构造特定的测试场景(例如,发送无效数据、测试边界条件、注入特定测试值)非常有用。 对于每个 API 端点,测试编排器会先尝试根据 API 规范(YAPI/Swagger)生成一个"基线"的请求(包含必要的参数、基于 schema 的请求体等)。然后,您的测试用例的 `generate_*` 方法会被调用,并传入这个基线数据作为参数,您可以对其进行修改。 1. **`generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]`** * **何时调用**: 在确定请求的查询参数时。 * **输入**: `current_query_params` - 一个字典,包含测试编排器根据 API 规范(例如 `endpoint_spec['req_query']`)和可能的默认值生成的当前查询参数。 * **输出**: 您必须返回一个字典,该字典将作为最终发送请求时使用的查询参数。您可以添加、删除或修改 `current_query_params` 中的条目。 * **用途**: 注入特定的查询参数值,测试不同的过滤条件、分页参数组合等。 2. **`generate_headers(self, current_headers: Dict[str, str]) -> Dict[str, str]`** * **何时调用**: 在确定请求头时。 * **输入**: `current_headers` - 一个字典,包含测试编排器生成的当前请求头(可能包含如 `Content-Type`, `Accept` 等默认头,以及 API 规范中定义的请求头)。 * **输出**: 您必须返回一个字典,作为最终的请求头。 * **用途**: 添加/修改认证令牌 (`Authorization`)、租户ID (`X-Tenant-ID`)、自定义测试头等。 3. **`generate_request_body(self, current_body: Optional[Any]) -> Optional[Any]`** * **何时调用**: 在确定请求体时 (主要用于 `POST`, `PUT`, `PATCH` 等方法)。 * **输入**: `current_body` - 测试编排器根据 API 规范中的请求体 schema (例如 `endpoint_spec['req_body_other']` for YAPI JSON body, 或 Swagger requestBody schema) 生成的请求体。可能是字典/列表 (对于JSON),字符串或其他类型。 * **输出**: 您必须返回最终要发送的请求体。 * **用途**: 构造特定的请求体数据,例如: * 发送缺少必填字段的数据。 * 发送类型不匹配的数据。 * 发送超出范围的数值。 * 注入用于测试特定业务逻辑的数据。 ### 3.3 请求预校验方法 这些方法在 API 请求的各个部分(URL、头、体)完全构建完成之后,但在实际发送到服务器之前被调用。这允许您在请求发出前对其进行最终的静态检查。 每个预校验方法都应返回一个 `List[ValidationResult]`。 1. **`validate_request_url(self, url: str, request_context: APIRequestContext) -> List[ValidationResult]`** * **何时调用**: 请求的完整 URL 构建完毕后。 * **输入**: * `url: str`: 最终构建的、将要发送的完整请求 URL。 * `request_context: APIRequestContext`: 包含当前请求的详细上下文信息(见 4.2 节)。 * **用途**: 检查 URL 格式是否符合规范(例如 RESTful 路径结构 `/api/{version}/{resource}`)、路径参数是否正确编码、查询参数是否符合命名规范(如全小写+下划线)等。 2. **`validate_request_headers(self, headers: Dict[str, str], request_context: APIRequestContext) -> List[ValidationResult]`** * **何时调用**: 请求头完全确定后。 * **输入**: * `headers: Dict[str, str]`: 最终将要发送的请求头。 * `request_context: APIRequestContext`: 当前请求的上下文。 * **用途**: 检查是否包含所有必要的请求头 (`X-Tenant-ID`, `Authorization`)、头部字段值是否符合特定格式或约定。 3. **`validate_request_body(self, body: Optional[Any], request_context: APIRequestContext) -> List[ValidationResult]`** * **何时调用**: 请求体完全确定后。 * **输入**: * `body: Optional[Any]`: 最终将要发送的请求体。 * `request_context: APIRequestContext`: 当前请求的上下文。 * **用途**: 对最终的请求体进行静态检查,例如,检查 JSON 结构是否与预期一致(不一定是严格的 schema 验证,因为那通常在 `generate_request_body` 或由框架处理,但可以做一些更具体的业务逻辑检查)。 ### 3.4 响应验证方法 这是最核心的验证阶段,在从服务器接收到 API 响应后调用。 1. **`validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]`** * **何时调用**: 收到 API 响应后。 * **输入**: * `response_context: APIResponseContext`: 包含 API 响应的详细上下文信息(见 4.3 节),如状态码、响应头、响应体内容等。 * `request_context: APIRequestContext`: 触发此响应的原始请求的上下文。 * **输出**: 返回一个 `List[ValidationResult]`,包含对该响应的所有验证点的结果。 * **用途**: 这是进行绝大多数验证的地方,例如: * 检查 HTTP 状态码是否符合预期。 * 验证响应头是否包含特定字段及其值。 * 对响应体内容进行 JSON Schema 验证(可以调用框架提供的 `JSONSchemaValidator`)。 * 验证响应体中的具体数据是否符合业务规则。 * 检查错误响应的结构和错误码是否正确。 ### 3.5 性能与附加检查方法 (可选) 1. **`check_performance(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]`** * **何时调用**: 收到 API 响应后,通常在主要的 `validate_response` 之后。 * **输入**: 与 `validate_response` 相同。 * **输出**: 返回一个 `List[ValidationResult]`。 * **用途**: 执行与性能相关的检查,最常见的是检查 API 的响应时间 (`response_context.elapsed_time`) 是否在可接受的阈值内。 ## 4. 核心辅助类 这些类是 `BaseAPITestCase` 的重要组成部分,用于传递信息和报告结果。 ### 4.1 `ValidationResult` ```python class ValidationResult: def __init__(self, passed: bool, message: str, details: Optional[Dict[str, Any]] = None): self.passed: bool # True 表示验证通过, False 表示失败 self.message: str # 对验证结果的描述性消息 self.details: Dict[str, Any] # 可选的字典,用于存储额外信息,如实际值、期望值、上下文等 ``` * **用途**: 所有 `validate_*` 和 `check_*` 方法都应返回一个此对象的列表。每个对象代表一个具体的检查点。 * **示例**: ```python results.append(ValidationResult(passed=True, message="状态码为 200 OK。")) results.append(ValidationResult( passed=False, message=f"用户ID不匹配。期望: '{expected_id}', 实际: '{actual_id}'", details={"expected": expected_id, "actual": actual_id} )) ``` ### 4.2 `APIRequestContext` ```python class APIRequestContext: def __init__(self, method: str, url: str, path_params: Dict[str, Any], query_params: Dict[str, Any], headers: Dict[str, str], body: Optional[Any]): self.method: str # HTTP 方法 (e.g., "GET", "POST") self.url: str # 完整的请求 URL self.path_params: Dict[str, Any] # 从路径中解析出的参数及其值 self.query_params: Dict[str, Any]# 最终使用的查询参数 self.headers: Dict[str, str] # 最终使用的请求头 self.body: Optional[Any] # 最终使用的请求体 ``` * **用途**: 在请求相关的钩子方法中提供关于已构建请求的全面信息。 ### 4.3 `APIResponseContext` ```python class APIResponseContext: def __init__(self, status_code: int, headers: Dict[str, str], json_content: Optional[Any], text_content: Optional[str], elapsed_time: float, original_response: Any): self.status_code: int # HTTP 响应状态码 (e.g., 200, 404) self.headers: Dict[str, str] # 响应头 self.json_content: Optional[Any]# 如果响应是JSON且成功解析,则为解析后的对象 (字典或列表),否则为 None self.text_content: Optional[str]# 原始响应体文本内容 self.elapsed_time: float # API 调用耗时 (从发送请求到收到完整响应头),单位:秒 self.original_response: Any # 底层 HTTP 库返回的原始响应对象 (例如 `requests.Response`),供高级用例使用 ``` * **用途**: 在响应相关的钩子方法中提供关于收到的 API 响应的全面信息。 ### 4.4 `TestSeverity` 枚举 ```python from enum import Enum class TestSeverity(Enum): CRITICAL = "严重" HIGH = "高" MEDIUM = "中" LOW = "低" INFO = "信息" ``` * **用途**: 用于定义测试用例的严重级别,方便报告和结果分析。 ## 5. 日志记录 `BaseAPITestCase` 在其 `__init__` 方法中为每个测试用例实例初始化了一个标准的 Python logger: `self.logger = logging.getLogger(f"testcase.{self.id}")` 您可以在测试用例的任何方法中使用 `self.logger` 来输出调试信息、执行流程或遇到的问题。 **示例**: ```python self.logger.info(f"正在为端点 {self.endpoint_spec['path']} 生成请求体...") if error_condition: self.logger.warning(f"在为 {self.endpoint_spec['title']} 处理数据时遇到警告: {error_condition}") ``` 这些日志将由应用程序的整体日志配置进行管理。 ## 6. 简单示例:检查状态码和响应时间 ```python # In custom_testcases/basic_response_checks.py from your_project.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext class StatusCode200Check(BaseAPITestCase): id = "TC-STATUS-001" name = "状态码 200 OK 检查" description = "验证 API 是否成功响应并返回状态码 200。" severity = TestSeverity.CRITICAL tags = ["status_code", "smoke"] # 此测试用例适用于所有端点,因此无需定义 applicable_methods 或 applicable_paths_regex def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> list[ValidationResult]: results = [] if response_context.status_code == 200: results.append(ValidationResult(passed=True, message="响应状态码为 200 OK。")) else: results.append(ValidationResult( passed=False, message=f"期望状态码 200,但收到 {response_context.status_code}。", details={ "expected_status": 200, "actual_status": response_context.status_code, "response_body_sample": (response_context.text_content or "")[:200] # 包含部分响应体以帮助诊断 } )) return results class ResponseTimeCheck(BaseAPITestCase): id = "TC-PERF-001" name = "API 响应时间检查 (小于1秒)" description = "验证 API 响应时间是否在 1000 毫秒以内。" severity = TestSeverity.MEDIUM tags = ["performance"] MAX_RESPONSE_TIME_SECONDS = 1.0 # 1 秒 def check_performance(self, response_context: APIResponseContext, request_context: APIRequestContext) -> list[ValidationResult]: results = [] elapsed_ms = response_context.elapsed_time * 1000 if response_context.elapsed_time <= self.MAX_RESPONSE_TIME_SECONDS: results.append(ValidationResult( passed=True, message=f"响应时间 {elapsed_ms:.2f}ms,在阈值 {self.MAX_RESPONSE_TIME_SECONDS*1000:.0f}ms 以内。" )) else: results.append(ValidationResult( passed=False, message=f"响应时间过长: {elapsed_ms:.2f}ms。期望小于 {self.MAX_RESPONSE_TIME_SECONDS*1000:.0f}ms。", details={"actual_ms": elapsed_ms, "threshold_ms": self.MAX_RESPONSE_TIME_SECONDS*1000} )) return results ``` ## 7. 最佳实践和注意事项 * **保持测试用例的单一职责**:尽量让每个 `APITestCase` 类专注于一个特定的验证目标或一小组紧密相关的检查点。这使得测试用例更易于理解、维护和调试。 * **清晰的命名**:为您的测试用例类、`id` 和 `name` 使用清晰、描述性的名称。 * **充分利用 `endpoint_spec`**:在测试逻辑中,参考 `self.endpoint_spec` 来了解 API 的预期行为、参数、schema 等,使您的测试更加精确。 * **详细的 `ValidationResult` 消息**:当验证失败时,提供足够详细的 `message` 和 `details`,以便快速定位问题。 * **考虑性能**:虽然灵活性是关键,但避免在测试用例中执行过于耗时的操作,除非是专门的性能测试。 * **错误处理**:在您的测试用例代码中妥善处理可能发生的异常,并使用 `self.logger` 记录它们。 * **可重用逻辑**:如果多个测试用例需要相似的逻辑(例如,解析特定的响应结构、生成特定的测试数据),考虑将这些逻辑提取到共享的辅助函数或一个共同的基类中(您的测试用例可以继承自这个中间基类,而这个中间基类继承自 `BaseAPITestCase`)。 * **逐步实现**:从简单的测试用例开始,逐步构建更复杂的验证逻辑。 通过遵循本指南,您将能够有效地利用 `APITestCase` 机制为您的 DDMS 合规性验证软件构建强大而灵活的自动化测试。