21 KiB
APITestCase 开发指南
本文档旨在指导开发人员如何创建和使用自定义的 APITestCase 类,以扩展 DDMS 合规性验证软件的测试能力。通过继承 BaseAPITestCase,您可以编写灵活且强大的 Python 代码来定义针对 API 的各种验证逻辑。
1. APITestCase 概述
APITestCase 是 DDMS 合规性验证软件中定义具体测试逻辑的核心单元。每个派生自 BaseAPITestCase 的类代表一个独立的测试场景或一组相关的检查点,例如验证特定的请求头、检查响应状态码、或确保响应体符合特定业务规则。
核心理念:
- 代码即测试:使用 Python 的全部功能来定义复杂的测试逻辑,摆脱传统基于配置或简单规则的限制。
- 灵活性:允许测试用例在 API 请求的各个阶段介入,包括请求数据的生成、请求发送前的预校验、以及响应接收后的深度验证。
- 可重用性与模块化:常见的验证逻辑可以封装在辅助函数或基类中,方便在多个测试用例间共享。
在测试执行期间,测试编排器(APITestOrchestrator)会自动发现、加载并执行适用于当前 API 端点的所有已注册的 APITestCase 实例。
2. 如何创建自定义测试用例
创建一个新的测试用例涉及以下步骤:
- 创建 Python 文件:在指定的测试用例目录(例如
custom_testcases/)下创建一个新的.py文件。建议文件名能反映其测试内容,例如header_validation_tests.py。 - 继承
BaseAPITestCase:在文件中定义一个或多个类,使其继承自your_project.test_framework_core.BaseAPITestCase(请替换为实际路径)。 - 定义元数据:在您的自定义测试用例类中,必须定义以下类属性:
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"]。
- 可选:控制适用范围:您可以选择性地定义以下类属性来限制测试用例的应用范围:
applicable_methods: Optional[List[str]]: 一个 HTTP 方法字符串的列表(大写),例如["POST", "PUT"]。如果定义了此属性,则该测试用例仅应用于具有这些方法的 API 端点。如果为None(默认),则适用于所有方法。applicable_paths_regex: Optional[str]: 一个 Python 正则表达式字符串。如果定义了此属性,则该测试用例仅应用于其路径与此正则表达式匹配的 API 端点。如果为None(默认),则适用于所有路径。
- 实现验证逻辑:重写
BaseAPITestCase中一个或多个generate_*或validate_*方法来实现您的具体测试逻辑。
示例骨架:
# 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__)
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_* 方法会被调用,并传入这个基线数据作为参数,您可以对其进行修改。
-
generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]- 何时调用: 在确定请求的查询参数时。
- 输入:
current_query_params- 一个字典,包含测试编排器根据 API 规范(例如endpoint_spec['req_query'])和可能的默认值生成的当前查询参数。 - 输出: 您必须返回一个字典,该字典将作为最终发送请求时使用的查询参数。您可以添加、删除或修改
current_query_params中的条目。 - 用途: 注入特定的查询参数值,测试不同的过滤条件、分页参数组合等。
-
generate_headers(self, current_headers: Dict[str, str]) -> Dict[str, str]- 何时调用: 在确定请求头时。
- 输入:
current_headers- 一个字典,包含测试编排器生成的当前请求头(可能包含如Content-Type,Accept等默认头,以及 API 规范中定义的请求头)。 - 输出: 您必须返回一个字典,作为最终的请求头。
- 用途: 添加/修改认证令牌 (
Authorization)、租户ID (X-Tenant-ID)、自定义测试头等。
-
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]。
-
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})、路径参数是否正确编码、查询参数是否符合命名规范(如全小写+下划线)等。
-
validate_request_headers(self, headers: Dict[str, str], request_context: APIRequestContext) -> List[ValidationResult]- 何时调用: 请求头完全确定后。
- 输入:
headers: Dict[str, str]: 最终将要发送的请求头。request_context: APIRequestContext: 当前请求的上下文。
- 用途: 检查是否包含所有必要的请求头 (
X-Tenant-ID,Authorization)、头部字段值是否符合特定格式或约定。
-
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 响应后调用。
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 性能与附加检查方法 (可选)
check_performance(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]- 何时调用: 收到 API 响应后,通常在主要的
validate_response之后。 - 输入: 与
validate_response相同。 - 输出: 返回一个
List[ValidationResult]。 - 用途: 执行与性能相关的检查,最常见的是检查 API 的响应时间 (
response_context.elapsed_time) 是否在可接受的阈值内。
- 何时调用: 收到 API 响应后,通常在主要的
4. 核心辅助类
这些类是 BaseAPITestCase 的重要组成部分,用于传递信息和报告结果。
4.1 ValidationResult
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_*方法都应返回一个此对象的列表。每个对象代表一个具体的检查点。 - 示例:
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
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
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 枚举
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 来输出调试信息、执行流程或遇到的问题。
示例:
self.logger.info(f"正在为端点 {self.endpoint_spec['path']} 生成请求体...")
if error_condition:
self.logger.warning(f"在为 {self.endpoint_spec['title']} 处理数据时遇到警告: {error_condition}")
这些日志将由应用程序的整体日志配置进行管理。
6. 简单示例:检查状态码和响应时间
# 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 合规性验证软件构建强大而灵活的自动化测试。