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