23 KiB
技术架构概览
本 API 合规性测试框架主要由以下几个核心组件构成,它们协同工作以完成测试的定义、发现、执行和报告:
-
命令行接口 (
run_api_tests.py):- 作为测试执行的入口。
- 负责解析用户通过命令行传入的参数,例如 API 服务的基础 URL、API 规范文件路径(YAPI 或 Swagger)、测试用例目录、输出报告配置以及 LLM 相关配置。
- 初始化并驱动
APITestOrchestrator。
-
测试编排器 (
APITestOrchestrator在ddms_compliance_suite/test_orchestrator.py):- 核心控制器:是整个测试流程的指挥中心。
- 组件初始化:负责初始化和管理其他关键组件,如
InputParser(API 规范解析器)、APICaller(API 请求调用器)、TestCaseRegistry(测试用例注册表)以及可选的LLMService(大模型服务)。 $ref解析: 在将API端点规范 (endpoint_spec_dict) 传递给测试用例构造函数之前,会使用schema_utils.resolve_json_schema_references解析其中的 schemas (requestBody, parameters, responses),默认会丢弃原始的$ref和$$前缀的键。- 测试流程管理:
- 调用
InputParser解析指定的 API 规范文件,获取所有端点的定义。 - 根据用户指定的过滤器(如 YAPI 分类或 Swagger 标签)筛选需要测试的 API 端点。
- 对每一个选定的 API 端点:
- 通过
TestCaseRegistry获取所有适用于该端点的自定义测试用例类。 - 实例化每个测试用例类。
- 调用
_prepare_initial_request_data方法准备初始请求数据(路径参数、查询参数、请求头、请求体)。此方法会根据全局配置和测试用例自身的配置决定是否使用 LLM 进行数据生成,并利用LLMService和动态 Pydantic 模型创建(_create_pydantic_model_from_schema)来实现。如果LLM未启用或不适用,则使用传统的基于 Schema 的数据生成逻辑(_generate_params_from_list,_generate_parameters_from_schema)。此阶段还实现了端点级别的LLM参数缓存。 - 依次调用测试用例实例中定义的
generate_*方法,允许测试用例修改生成的请求数据。 - 调用测试用例实例中定义的
validate_request_*方法,对即将发送的请求进行预校验。 - 使用
APICaller发送最终构建的 API 请求。 - 接收到 API 响应后,调用测试用例实例中定义的
validate_response和check_performance方法,对响应进行详细验证。
- 通过
- 调用
- 结果汇总:收集每个测试用例的执行结果 (
ExecutedTestCaseResult),汇总成每个端点的测试结果 (TestResult),并最终生成整个测试运行的摘要 (TestSummary)。
-
测试用例注册表 (
TestCaseRegistry在ddms_compliance_suite/test_case_registry.py):- 动态发现:负责在用户指定的目录 (
custom_test_cases_dir) 下扫描并动态加载所有以.py结尾的测试用例文件。 - 类识别与注册:从加载的模块中,识别出所有继承自
BaseAPITestCase的类,并根据其id属性进行注册。 - 执行顺序排序:在发现所有测试用例类后,会根据每个类的
execution_order属性(主排序键,升序)和类名__name__(次排序键,字母升序)对它们进行排序。 - 适用性筛选:提供
get_applicable_test_cases方法,根据 API 端点的 HTTP 方法和路径(通过正则表达式匹配)筛选出适用的、已排序的测试用例类列表给编排器。
- 动态发现:负责在用户指定的目录 (
-
测试框架核心 (
test_framework_core.py):BaseAPITestCase:所有自定义测试用例的基类。它定义了测试用例应具备的元数据(如id,name,description,severity,tags,execution_order,applicable_methods,applicable_paths_regex以及 LLM 使用标志位)和一系列生命周期钩子方法(如generate_*,validate_*),以及众多辅助方法(见下文)。APIRequestContext/APIResponseContext:数据类,分别用于封装 API 请求和响应的上下文信息,在测试用例的钩子方法间传递。ValidationResult:数据类,用于表示单个验证点的结果(通过/失败、消息、详细信息)。TestSeverity:枚举类型,定义测试用例的严重级别。
-
API 规范解析器 (
InputParser在ddms_compliance_suite/input_parser/parser.py):- 负责读取和解析 YAPI(JSON 格式)或 Swagger/OpenAPI(JSON 或 YAML 格式)的 API 规范文件。
- 将原始规范数据转换成框架内部易于处理的结构化对象(如
ParsedYAPISpec,YAPIEndpoint,ParsedSwaggerSpec,SwaggerEndpoint)。
-
API 调用器 (
APICaller在ddms_compliance_suite/api_caller/caller.py):- 封装了实际的 HTTP 请求发送逻辑。
- 接收一个
APIRequest对象(包含方法、URL、参数、头部、请求体),使用如requests库执行请求,并返回一个APIResponse对象(包含状态码、响应头、响应体内容等)。
-
LLM 服务 (
LLMService在ddms_compliance_suite/llm_utils/llm_service.py) (可选):- 如果配置了 LLM 服务(如通义千问的兼容 OpenAI 模式的 API),此组件负责与 LLM API 交互。
- 主要用于根据 Pydantic 模型(从 JSON Schema 动态创建)智能生成复杂的请求参数或请求体。
-
工具模块 (
ddms_compliance_suite/utils/):schema_utils.py: 包含一系列用于处理 JSON Schema 和 API 参数的实用函数。common_utils.py: 包含通用的辅助函数。
这个架构旨在提供一个灵活、可扩展的 API 测试框架,允许用户通过编写自定义的 Python 测试用例来定义复杂的验证逻辑。
自定义 APITestCase 编写指南
此指南帮助您创建自定义的 APITestCase 类,以扩展 DDMS 合规性验证软件的测试能力。核心理念是 代码即测试,并充分利用框架提供的工具函数和基类辅助方法来简化测试用例的编写。
1. 创建自定义测试用例
-
创建 Python 文件:在您的自定义测试用例目录(例如
custom_testcases/)下创建一个新的.py文件。 -
导入必要模块:
from typing import Dict, Any, Optional, List from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext from ddms_compliance_suite.utils import schema_utils, common_utils # 导入工具模块 import logging # 可选,用于自定义日志 -
继承
BaseAPITestCase:定义一个或多个类,使其继承自BaseAPITestCase。 -
定义元数据 (类属性):
id: str: 测试用例的全局唯一标识符 (例如"TC-MYFEATURE-001")。name: str: 人类可读的名称。description: str: 详细描述。severity: TestSeverity: 严重程度 (例如TestSeverity.CRITICAL,TestSeverity.HIGH, 等)。tags: List[str]: 分类标签 (例如["smoke", "regression"])。execution_order: int: 控制测试用例的执行顺序。数值较小的会比较大的先执行。默认值为100。applicable_methods: Optional[List[str]]: 限制适用的 HTTP 方法 (例如["POST", "PUT"])。None表示所有方法。applicable_paths_regex: Optional[str]: 限制适用的 API 路径 (Python 正则表达式)。None表示所有路径。- LLM 使用标志 (可选): 这些标志允许测试用例覆盖全局 LLM 配置。
use_llm_for_body: bool = Falseuse_llm_for_path_params: bool = Falseuse_llm_for_query_params: bool = Falseuse_llm_for_headers: bool = False
-
实现
__init__(如果需要自定义初始化逻辑):- 通常,您会在这里调用基类的
__init__。 - 许多测试用例会在这里查找并设置测试目标字段/参数,可以利用
BaseAPITestCase提供的辅助方法(如_get_resolved_request_body_schema,_find_simple_type_field_in_schema,_find_first_simple_type_parameter,_find_removable_field_path)来完成。
class MissingRequiredFieldBodyCase(BaseAPITestCase): # ...元数据... def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[Any] = None): super().__init__(endpoint_spec, global_api_spec, json_schema_validator, llm_service) self.removable_field_path: Optional[List[Union[str, int]]] = None body_schema = self._get_resolved_request_body_schema() if body_schema: self.removable_field_path = self._find_removable_field_path(body_schema, "request body") if not self.removable_field_path: self.logger.info(f"[{self.id}] No removable required field found in request body.") - 通常,您会在这里调用基类的
-
实现验证逻辑:重写
BaseAPITestCase中一个或多个generate_*或validate_*方法。在这些方法中,积极使用框架提供的工具函数和基类辅助方法。
2. BaseAPITestCase 核心生命周期方法
这些方法由测试编排器在测试执行的不同阶段调用。
-
__init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any], llm_service: Optional[Any]):- 构造函数。
endpoint_spec包含当前测试端点的 API 定义(已经过$ref解析),global_api_spec包含完整的 API 规范。 - 基类会初始化
self.logger,可用于记录日志。json_schema_validator和llm_service也会被存储。
- 构造函数。
-
请求生成与修改方法: 在 API 请求发送前调用,用于修改或生成请求数据。
generate_path_params(self, current_path_params: Dict[str, Any]) -> Dict[str, Any]generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]generate_headers(self, current_headers: Dict[str, str]) -> Dict[str, str]generate_request_body(self, current_body: Optional[Any]) -> Optional[Any]modify_request_url(self, current_url: str) -> str
-
请求预校验方法: 在请求数据完全构建后、发送前调用,用于静态检查。返回
List[ValidationResult]。validate_request_url(self, url: str, request_context: APIRequestContext) -> List[ValidationResult]validate_request_headers(self, headers: Dict[str, str], request_context: APIRequestContext) -> List[ValidationResult]validate_request_body(self, body: Optional[Any], request_context: APIRequestContext) -> List[ValidationResult]
-
响应验证方法: 在收到 API 响应后调用,这是最主要的验证阶段。返回
List[ValidationResult]。validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]- 检查状态码、响应头、响应体内容是否符合预期。
- 进行业务逻辑相关的断言。
-
性能检查方法:
check_performance(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]- 通常用于检查响应时间
response_context.elapsed_time。
- 通常用于检查响应时间
3. BaseAPITestCase 提供的辅助方法
这些方法旨在简化测试用例中常见的任务:
-
_get_resolved_request_body_schema(self) -> Optional[Dict[str, Any]]:- 从
self.endpoint_spec中提取请求体的 schema。它会自动处理常见的 content-type (如application/json) 并考虑 OpenAPI 2.0 的in: body参数风格。 - 返回的 schema 已经由编排器进行过
$ref解析。
- 从
-
_find_removable_field_path(self, schema_to_search: Optional[Dict[str, Any]], schema_name_for_log: str) -> Optional[List[Union[str, int]]]:- 在给定的
schema_to_search中查找第一个可移除的必填字段的路径。 - 内部调用
schema_utils.util_find_removable_field_path_recursive。 - 适用于构造"缺失必填字段"之类的测试场景。
schema_name_for_log用于日志记录,例如 "request body"。
- 在给定的
-
_find_simple_type_field_in_schema(self, schema_to_search: Optional[Dict[str, Any]], schema_name_for_log: str) -> Optional[Tuple[List[Union[str, int]], str, Dict[str, Any]]]:- 在给定的
schema_to_search(应为已解析的字典) 中查找第一个简单类型 (string, integer, number, boolean) 字段。 - 内部调用
schema_utils.find_first_simple_type_field_recursive。 - 返回一个元组
(field_path, field_type, field_schema)或None。 - 适用于构造"字段类型不匹配"之类的测试场景。
- 在给定的
-
_find_first_simple_type_parameter(self, param_location: str) -> Optional[Tuple[List[Union[str, int]], str, Dict[str, Any], str]]:- 在指定的参数位置 (
param_location,如 "query", "header") 查找第一个简单类型的参数或参数内部的简单类型字段。 - 它会检查参数是否直接是简单类型,或者其
schema是否为简单类型或包含简单类型的对象。 - 如果参数的 schema 是对象,它会调用
_find_simple_type_field_in_schema来查找嵌套字段。 - 返回
(full_path, param_type, param_schema, top_level_param_name)或None。 full_path可能是['paramName']或['paramName', 'nestedField']。
- 在指定的参数位置 (
-
_find_required_parameter_name(self, param_in: str) -> Optional[str]:- 查找指定位置 (
param_in,如 "query", "header", "path") 的第一个必填参数的名称。
- 查找指定位置 (
-
expect_error_response(self, response_context: APIResponseContext, expected_status_codes: List[int], expected_error_code_in_body: Optional[Union[str, int]] = None, error_code_field_name: str = "code", context_message_prefix: str = "Error response validation") -> List[ValidationResult](新增):- 一个标准化的方法来验证错误响应。
- 检查
response_context.status_code是否在expected_status_codes列表中。 - 可选地,检查响应体 (如果是JSON对象) 中
error_code_field_name字段的值是否等于expected_error_code_in_body。 - 返回包含详细信息的
ValidationResult列表。 - 示例:
# 在某个测试用例的 validate_response 中 if not self.target_field_path: # 假设此用例需要一个目标字段 return [self.passed("Skipped: No target field identified.")] field_desc = f"body field '{'.'.join(map(str, self.target_field_path))}'" return self.expect_error_response( response_context=response_context, expected_status_codes=[400, 422], expected_error_code_in_body="4001", context_message_prefix=f"Missing required {field_desc}" )
-
validate_data_against_schema(self, data_to_validate: Any, schema_definition: Dict[str, Any], context_message_prefix: str = "Data") -> List[ValidationResult]:- 使用注入的
JSONSchemaValidator(如果可用) 来验证数据是否符合给定的 schema。
- 使用注入的
-
passed(message: str, details: Optional[Dict[str, Any]] = None) -> ValidationResult(静态方法):- 快速创建表示"通过"的
ValidationResult。
- 快速创建表示"通过"的
-
failed(message: str, details: Optional[Dict[str, Any]] = None) -> ValidationResult(静态方法):- 快速创建表示"失败"的
ValidationResult。
- 快速创建表示"失败"的
4. ddms_compliance_suite.utils 工具模块
4.1 schema_utils.py
这个模块包含处理 JSON Schema、API 参数和数据结构的函数。
-
resolve_json_schema_references(schema_to_resolve: Any, full_api_spec: Dict[str, Any], ..., discard_refs: bool = True) -> Any:- 递归解析 JSON Schema 中的
$ref引用。 discard_refs=True(默认) 会在解析前移除$ref和以$$开头的键。如果为False,则尝试解析$ref并保留其他键。- 此函数主要由测试编排器在实例化测试用例前调用。测试用例通常不需要直接使用它,因为
endpoint_spec已经预处理过了。
- 递归解析 JSON Schema 中的
-
util_find_removable_field_path_recursive(current_schema: Dict[str, Any], current_path: List[Union[str, int]], full_api_spec_for_refs: Dict[str, Any]) -> Optional[List[Union[str, int]]]:- 递归地在(可能包含
$ref的)current_schema中查找第一个可移除的必填字段的路径。 - 主要被
BaseAPITestCase._find_removable_field_path调用。
- 递归地在(可能包含
-
util_remove_value_at_path(data_container: Any, path: List[Union[str, int]]) -> Tuple[Any, Any, bool]:- 从嵌套的字典/列表中移除指定路径的值。
- 返回
(修改后的容器, 被移除的值, 是否成功)。 - 示例:
# 在某个测试用例的 generate_request_body 中 if self.removable_field_path: modified_body, removed_value, success = schema_utils.util_remove_value_at_path( current_body, self.removable_field_path ) if success: self.logger.info(f"Removed value '{removed_value}' from path '{'.'.join(map(str, self.removable_field_path))}'.") return modified_body return current_body
-
util_set_value_at_path(data_container: Any, path: List[Union[str, int]], new_value: Any) -> Tuple[Any, bool](新增):- 在嵌套的字典/列表中为指定路径设置或修改
new_value。 - 如果路径中的父级结构不存在,会尝试创建它们(字典会被创建,列表会用
None填充到所需索引)。 - 返回
(修改后的容器, 是否成功)。 - 示例:
# 在某个测试用例的 generate_request_body 中 if self.target_field_path and self.mismatched_value is not None: modified_body, success = schema_utils.util_set_value_at_path( current_body, self.target_field_path, self.mismatched_value ) if success: return modified_body return current_body
- 在嵌套的字典/列表中为指定路径设置或修改
-
generate_mismatched_value(original_type: Optional[str], original_value: Any, field_schema: Optional[Dict[str, Any]], logger_param: Optional[logging.Logger] = None) -> Any(新增):- 根据字段的
original_type、original_value(当前未使用)和field_schema(用于检查enum等约束)生成一个类型不匹配的值。 - 适用于类型不匹配的测试场景。
- 示例:
# 在某个测试用例的 __init__ 或 generate_ 方法中 if self.original_field_type: # 假设 self.original_field_type 已被正确设置 self.mismatched_val = schema_utils.generate_mismatched_value( self.original_field_type, None, # original_value, 暂时可以为 None self.target_field_schema, # 字段的 schema self.logger )
- 根据字段的
-
build_object_schema_for_params(params_spec_list: List[Dict[str, Any]], model_name_base: str, ...) -> Tuple[Optional[Dict[str, Any]], str]:- 从参数规范列表(例如 OpenAPI 参数对象列表)构建一个聚合的 JSON 对象 schema。
- 可用于将多个查询参数或头部参数统一表示为一个对象 schema,方便进行 schema 校验或数据生成。
-
find_first_simple_type_field_recursive(current_schema: Dict[str, Any], ...) -> Optional[Tuple[List[Union[str, int]], str, Dict[str, Any]]]:- 递归地在已解析的
current_schema中查找第一个简单类型的字段 (string, integer, number, boolean),包括嵌套在对象或数组中的。 - 主要被
BaseAPITestCase._find_simple_type_field_in_schema调用。
- 递归地在已解析的
4.2 common_utils.py
-
format_url_with_path_params(base_url: str, path_template: str, path_params: Dict[str, Any]) -> str:- 将路径模板中的占位符 (如
{userId}) 替换为path_params中提供的值,并与base_url组合成完整的 URL。
- 将路径模板中的占位符 (如
5. 核心数据类
-
ValidationResult(passed: bool, message: str, details: Optional[Dict[str, Any]] = None):- 封装单个验证点的结果。所有
validate_*和check_*方法都应返回此对象的列表。
- 封装单个验证点的结果。所有
-
APIRequestContext: 包含当前请求的详细信息(方法、URL、参数、头、体、端点规范)。 -
APIResponseContext: 包含 API 响应的详细信息(状态码、头、JSON 内容、文本内容、耗时、原始响应对象、关联的请求上下文)。
6. 编写测试用例的推荐流程
-
明确测试目标:这个测试用例要验证什么?是参数处理、错误响应、数据格式还是业务逻辑?
-
选择合适的基类方法:
- 如果需要修改请求数据(参数、头部、请求体),重写相应的
generate_*方法。 - 如果需要在发送前对请求的静态结构进行校验,重写
validate_request_*方法。 - 核心的响应验证逻辑通常在
validate_response中实现。 - 性能相关的检查在
check_performance中。
- 如果需要修改请求数据(参数、头部、请求体),重写相应的
-
初始化 (在
__init__中):- 调用
super().__init__(...)。 - 如果测试依赖于特定的请求体字段或参数:
- 获取请求体 schema:
body_schema = self._get_resolved_request_body_schema()。 - 查找必填字段进行移除测试:
path = self._find_removable_field_path(body_schema, "request body")。 - 查找简单类型字段进行类型不匹配测试 (请求体):
target = self._find_simple_type_field_in_schema(body_schema, "request body")。 - 查找简单类型参数进行类型不匹配测试 (查询/头部):
target = self._find_first_simple_type_parameter("query")。 - 查找必填参数名称:
name = self._find_required_parameter_name("query")。
- 获取请求体 schema:
- 将找到的目标路径、类型、schema 等信息存储在
self的属性中,供后续方法使用。
- 调用
-
生成/修改请求数据 (在
generate_*方法中):- 如果需要移除字段,使用
schema_utils.util_remove_value_at_path()。 - 如果需要修改字段值(例如进行类型不匹配测试):
- 使用
schema_utils.generate_mismatched_value()生成不匹配的值。 - 使用
schema_utils.util_set_value_at_path()将该值设置到请求数据中。
- 使用
- 如果需要移除字段,使用
-
验证响应 (在
validate_response方法中):- 如果测试期望一个错误响应,优先使用
self.expect_error_response()。 - 如果需要验证响应体数据是否符合特定的 JSON Schema,使用
self.validate_data_against_schema()。 - 对于其他自定义的断言,直接比较
response_context中的属性(如status_code,json_content)并使用self.passed()或self.failed()创建ValidationResult。
- 如果测试期望一个错误响应,优先使用
-
日志记录: 在关键步骤使用
self.logger.info(),self.logger.debug(),self.logger.warning()等记录信息,便于调试。
通过遵循这些指南并善用框架提供的工具,您可以更高效地编写出简洁、健壮且易于维护的 API 合规性测试用例。