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

21 KiB
Raw Blame History

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_* 方法来实现您的具体测试逻辑。

示例骨架:

# 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_specself.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

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 类专注于一个特定的验证目标或一小组紧密相关的检查点。这使得测试用例更易于理解、维护和调试。
  • 清晰的命名:为您的测试用例类、idname 使用清晰、描述性的名称。
  • 充分利用 endpoint_spec:在测试逻辑中,参考 self.endpoint_spec 来了解 API 的预期行为、参数、schema 等,使您的测试更加精确。
  • 详细的 ValidationResult 消息:当验证失败时,提供足够详细的 messagedetails,以便快速定位问题。
  • 考虑性能:虽然灵活性是关键,但避免在测试用例中执行过于耗时的操作,除非是专门的性能测试。
  • 错误处理:在您的测试用例代码中妥善处理可能发生的异常,并使用 self.logger 记录它们。
  • 可重用逻辑:如果多个测试用例需要相似的逻辑(例如,解析特定的响应结构、生成特定的测试数据),考虑将这些逻辑提取到共享的辅助函数或一个共同的基类中(您的测试用例可以继承自这个中间基类,而这个中间基类继承自 BaseAPITestCase)。
  • 逐步实现:从简单的测试用例开始,逐步构建更复杂的验证逻辑。

通过遵循本指南,您将能够有效地利用 APITestCase 机制为您的 DDMS 合规性验证软件构建强大而灵活的自动化测试。