compliance/docs/APITestCase_Development_Guide.md
gongwenxin 331e397367 step1
2025-05-19 15:57:35 +08:00

330 lines
21 KiB
Markdown
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.

# 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 合规性验证软件构建强大而灵活的自动化测试。