添加error 测试用例,但是测试用例太复杂,还需要优化框架

This commit is contained in:
gongwenxin
2025-05-23 12:05:48 +08:00
parent 0e3e721bc0
commit 4180a0ce81
37 changed files with 45453 additions and 499 deletions
+44 -31
View File
@@ -26,45 +26,58 @@ class TestCaseRegistry:
def discover_test_cases(self):
"""
扫描指定目录,动态导入模块,并注册所有继承自 BaseAPITestCase 的类。
扫描指定目录及其所有子目录,动态导入模块,并注册所有继承自 BaseAPITestCase 的类。
"""
if not os.path.isdir(self.test_cases_dir):
self.logger.warning(f"测试用例目录不存在或不是一个目录: {self.test_cases_dir}")
return
self.logger.info(f"开始从目录 '{self.test_cases_dir}' 发现测试用例...")
self.logger.info(f"开始从目录 '{self.test_cases_dir}' 及其子目录发现测试用例...")
found_count = 0
for filename in os.listdir(self.test_cases_dir):
if filename.endswith(".py") and not filename.startswith("__"):
module_name = filename[:-3]
file_path = os.path.join(self.test_cases_dir, filename)
try:
# 动态导入模块
spec = importlib.util.spec_from_file_location(module_name, file_path)
if spec and spec.loader:
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
self.logger.debug(f"成功导入模块: {module_name}{file_path}")
# 使用 os.walk 进行递归扫描
for root_dir, _, files in os.walk(self.test_cases_dir):
for filename in files:
if filename.endswith(".py") and not filename.startswith("__"):
module_name = filename[:-3]
file_path = os.path.join(root_dir, filename)
try:
# 动态导入模块
spec = importlib.util.spec_from_file_location(module_name, file_path)
if spec and spec.loader:
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
self.logger.debug(f"成功导入模块: {module_name}{file_path}")
# 在模块中查找 BaseAPITestCase 的子类
for name, obj in inspect.getmembers(module):
if inspect.isclass(obj) and issubclass(obj, BaseAPITestCase) and obj is not BaseAPITestCase:
if obj.id in self._registry:
self.logger.warning(f"发现重复的测试用例 ID: '{obj.id}' (来自类 '{obj.__name__}' in {file_path})。之前的定义将被覆盖")
self._registry[obj.id] = obj
if obj not in self._test_case_classes: # 避免重复添加同一个类对象
# 在模块中查找 BaseAPITestCase 的子类
for name, obj in inspect.getmembers(module):
if inspect.isclass(obj) and issubclass(obj, BaseAPITestCase) and obj is not BaseAPITestCase:
if not hasattr(obj, 'id') or not obj.id:
self.logger.error(f"测试用例类 '{obj.__name__}' 在文件 '{file_path}' 中缺少有效的 'id' 属性,已跳过注册")
continue
if obj.id in self._registry:
self.logger.warning(f"发现重复的测试用例 ID: '{obj.id}' (来自类 '{obj.__name__}' in {file_path})。之前的定义将被覆盖。")
self._registry[obj.id] = obj
# 更新 _test_case_classes 列表:如果已存在相同ID的类,替换它;否则添加。
# 这确保了排序时使用的是最新的类定义,以防ID冲突。
existing_class_indices = [i for i, tc_class in enumerate(self._test_case_classes) if tc_class.id == obj.id]
if existing_class_indices:
for index in sorted(existing_class_indices, reverse=True): # 从后往前删除,避免索引问题
self.logger.debug(f"从 _test_case_classes 列表中移除旧的同ID ('{obj.id}') 测试用例类: {self._test_case_classes[index].__name__}")
del self._test_case_classes[index]
self._test_case_classes.append(obj)
found_count += 1
self.logger.info(f"已注册测试用例: '{obj.id}' ({obj.name}) 来自类 '{obj.__name__}'")
else:
self.logger.error(f"无法为文件 '{file_path}' 创建模块规范。")
except ImportError as e:
self.logger.error(f"导入模块 '{module_name}''{file_path}' 失败: {e}", exc_info=True)
except AttributeError as e:
self.logger.error(f"在模块 '{module_name}' ({file_path}) 中查找测试用例时出错 (可能是缺少必要的元数据如 'id'): {e}", exc_info=True)
except Exception as e:
self.logger.error(f"处理文件 '{file_path}' 时发生未知错误: {e}", exc_info=True)
found_count += 1
self.logger.info(f"已注册测试用例: '{obj.id}' ({getattr(obj, 'name', 'N/A')}) 来自类 '{obj.__name__}' (路径: {file_path})")
else:
self.logger.error(f"无法为文件 '{file_path}' 创建模块规范。")
except ImportError as e:
self.logger.error(f"导入模块 '{module_name}''{file_path}' 失败: {e}", exc_info=True)
except AttributeError as e:
self.logger.error(f"在模块 '{module_name}' ({file_path}) 中查找测试用例时出错: {e}", exc_info=True)
except Exception as e:
self.logger.error(f"处理文件 '{file_path}' 时发生未知错误: {e}", exc_info=True)
# 根据 execution_order 对收集到的测试用例类进行排序
try:
+184 -55
View File
@@ -13,6 +13,7 @@ from enum import Enum
import datetime
import datetime as dt
from uuid import UUID
from dataclasses import asdict as dataclass_asdict, is_dataclass # New import
from pydantic import BaseModel, Field, create_model
from pydantic.networks import EmailStr
@@ -621,54 +622,182 @@ class APITestOrchestrator:
global_api_spec: Union[ParsedYAPISpec, ParsedSwaggerSpec] # 整个API的规格
) -> ExecutedTestCaseResult:
"""
实例化并执行单个APITestCase
"""
tc_start_time = time.time()
validation_points: List[ValidationResult] = []
test_case_instance: Optional[BaseAPITestCase] = None
endpoint_spec_dict: Dict[str, Any]
# 确保 endpoint_spec 转换为字典,以便在测试用例和请求上下文中统一使用
if hasattr(endpoint_spec, 'to_dict') and callable(endpoint_spec.to_dict):
endpoint_spec_dict = endpoint_spec.to_dict()
elif isinstance(endpoint_spec, dict): # 如果它已经是字典 (例如从 OpenAPI 解析器直接过来)
endpoint_spec_dict = endpoint_spec
elif isinstance(endpoint_spec, (YAPIEndpoint, SwaggerEndpoint)): # 作为后备,从特定类型提取
self.logger.debug(f"Manually converting endpoint_spec of type {type(endpoint_spec).__name__} to dict.")
endpoint_spec_dict = {
"method": getattr(endpoint_spec, 'method', 'UNKNOWN_METHOD'),
"path": getattr(endpoint_spec, 'path', 'UNKNOWN_PATH'),
"title": getattr(endpoint_spec, 'title', getattr(endpoint_spec, 'summary', '')),
"summary": getattr(endpoint_spec, 'summary', ''),
"description": getattr(endpoint_spec, 'description', ''),
"operationId": getattr(endpoint_spec, 'operation_id',
f"{getattr(endpoint_spec, 'method', '').upper()}_{getattr(endpoint_spec, 'path', '').replace('/', '_')}"),
# 尝试提取参数和请求体 (简化版)
"parameters": getattr(endpoint_spec, 'parameters', []) if isinstance(endpoint_spec, SwaggerEndpoint) else (getattr(endpoint_spec, 'req_query', []) + getattr(endpoint_spec, 'req_headers', [])),
"requestBody": getattr(endpoint_spec, 'request_body', None) if isinstance(endpoint_spec, SwaggerEndpoint) else getattr(endpoint_spec, 'req_body_other', None),
"_original_object_type": type(endpoint_spec).__name__
}
else:
endpoint_spec_dict = {}
self.logger.warning(f"endpoint_spec无法转换为字典,实际类型: {type(endpoint_spec)}")
执行单个测试用例
global_api_spec_dict: Dict[str, Any]
if hasattr(global_api_spec, 'to_dict') and callable(global_api_spec.to_dict):
global_api_spec_dict = global_api_spec.to_dict()
elif isinstance(global_api_spec, dict):
global_api_spec_dict = global_api_spec
else:
global_api_spec_dict = {}
self.logger.warning(f"global_api_spec无法转换为字典,实际类型: {type(global_api_spec)}")
流程:
1. 准备请求数据 (路径参数, 查询参数, 请求头, 请求体)。
- 首先尝试从测试用例的 generate_xxx 方法获取。
- 如果测试用例未覆盖或返回None,则尝试从API spec生成默认数据。
- 如果开启了LLM,并且测试用例允许,则使用LLM生成。
2. (如果适用) 调用测试用例的 modify_request_url 钩子。
3. (如果适用) 调用测试用例的 validate_request_url, validate_request_headers, validate_request_body 钩子。
4. 发送API请求。
5. 记录响应。
6. 调用测试用例的 validate_response 和 check_performance 钩子。
7. 汇总验证结果,确定测试用例状态。
"""
start_time = time.monotonic()
validation_results: List[ValidationResult] = []
overall_status: ExecutedTestCaseResult.Status
execution_message = ""
# 将 endpoint_spec 转换为字典,如果它还不是的话
endpoint_spec_dict: Dict[str, Any]
if isinstance(endpoint_spec, dict):
endpoint_spec_dict = endpoint_spec
self.logger.debug(f"endpoint_spec 已经是字典类型。")
elif hasattr(endpoint_spec, 'to_dict') and callable(endpoint_spec.to_dict):
try:
endpoint_spec_dict = endpoint_spec.to_dict()
self.logger.debug(f"成功通过 to_dict() 方法将类型为 {type(endpoint_spec)} 的 endpoint_spec 转换为字典。")
if not endpoint_spec_dict: # 如果 to_dict() 返回空字典
self.logger.warning(f"endpoint_spec.to_dict() (类型: {type(endpoint_spec)}) 返回了一个空字典。")
# 尝试备用转换
if isinstance(endpoint_spec, (YAPIEndpoint, SwaggerEndpoint)):
self.logger.debug(f"尝试从 {type(endpoint_spec).__name__} 对象的属性手动构建 endpoint_spec_dict。")
endpoint_spec_dict = {
"method": getattr(endpoint_spec, 'method', 'UNKNOWN_METHOD').upper(),
"path": getattr(endpoint_spec, 'path', 'UNKNOWN_PATH'),
"title": getattr(endpoint_spec, 'title', getattr(endpoint_spec, 'summary', '')),
"summary": getattr(endpoint_spec, 'summary', ''),
"description": getattr(endpoint_spec, 'description', ''),
"operationId": getattr(endpoint_spec, 'operation_id', f"{getattr(endpoint_spec, 'method', '').upper()}_{getattr(endpoint_spec, 'path', '').replace('/', '_')}"),
"parameters": getattr(endpoint_spec, 'parameters', []) if hasattr(endpoint_spec, 'parameters') else (getattr(endpoint_spec, 'req_query', []) + getattr(endpoint_spec, 'req_headers', [])),
"requestBody": getattr(endpoint_spec, 'request_body', None) if hasattr(endpoint_spec, 'request_body') else getattr(endpoint_spec, 'req_body_other', None),
"_original_object_type": type(endpoint_spec).__name__
}
if not any(endpoint_spec_dict.values()): # 如果手动构建后仍基本为空
self.logger.error(f"手动从属性构建 endpoint_spec_dict (类型: {type(endpoint_spec)}) 后仍然为空或无效。")
endpoint_spec_dict = {} # 重置为空,触发下方错误处理
except Exception as e:
self.logger.error(f"调用 endpoint_spec (类型: {type(endpoint_spec)}) 的 to_dict() 方法时出错: {e}。尝试备用转换。")
if isinstance(endpoint_spec, (YAPIEndpoint, SwaggerEndpoint)):
self.logger.debug(f"尝试从 {type(endpoint_spec).__name__} 对象的属性手动构建 endpoint_spec_dict。")
endpoint_spec_dict = {
"method": getattr(endpoint_spec, 'method', 'UNKNOWN_METHOD').upper(),
"path": getattr(endpoint_spec, 'path', 'UNKNOWN_PATH'),
"title": getattr(endpoint_spec, 'title', getattr(endpoint_spec, 'summary', '')),
"summary": getattr(endpoint_spec, 'summary', ''),
"description": getattr(endpoint_spec, 'description', ''),
"operationId": getattr(endpoint_spec, 'operation_id', f"{getattr(endpoint_spec, 'method', '').upper()}_{getattr(endpoint_spec, 'path', '').replace('/', '_')}"),
"parameters": getattr(endpoint_spec, 'parameters', []) if hasattr(endpoint_spec, 'parameters') else (getattr(endpoint_spec, 'req_query', []) + getattr(endpoint_spec, 'req_headers', [])),
"requestBody": getattr(endpoint_spec, 'request_body', None) if hasattr(endpoint_spec, 'request_body') else getattr(endpoint_spec, 'req_body_other', None),
"_original_object_type": type(endpoint_spec).__name__
}
if not any(endpoint_spec_dict.values()): # 如果手动构建后仍基本为空
self.logger.error(f"手动从属性构建 endpoint_spec_dict (类型: {type(endpoint_spec)}) 后仍然为空或无效。")
endpoint_spec_dict = {} # 重置为空,触发下方错误处理
else:
endpoint_spec_dict = {} # 转换失败
elif hasattr(endpoint_spec, 'data') and isinstance(getattr(endpoint_spec, 'data'), dict): # 兼容 YAPIEndpoint 结构
endpoint_spec_dict = getattr(endpoint_spec, 'data')
self.logger.debug(f"使用了类型为 {type(endpoint_spec)} 的 endpoint_spec 的 .data 属性。")
else: # 如果没有 to_dict, 也不是已知可直接访问 .data 的类型,则尝试最后的通用转换或手动构建
if isinstance(endpoint_spec, (YAPIEndpoint, SwaggerEndpoint)):
self.logger.debug(f"类型为 {type(endpoint_spec).__name__} 的 endpoint_spec 没有 to_dict() 或 data,尝试从属性手动构建。")
endpoint_spec_dict = {
"method": getattr(endpoint_spec, 'method', 'UNKNOWN_METHOD').upper(),
"path": getattr(endpoint_spec, 'path', 'UNKNOWN_PATH'),
"title": getattr(endpoint_spec, 'title', getattr(endpoint_spec, 'summary', '')),
"summary": getattr(endpoint_spec, 'summary', ''),
"description": getattr(endpoint_spec, 'description', ''),
"operationId": getattr(endpoint_spec, 'operation_id', f"{getattr(endpoint_spec, 'method', '').upper()}_{getattr(endpoint_spec, 'path', '').replace('/', '_')}"),
"parameters": getattr(endpoint_spec, 'parameters', []) if hasattr(endpoint_spec, 'parameters') else (getattr(endpoint_spec, 'req_query', []) + getattr(endpoint_spec, 'req_headers', [])),
"requestBody": getattr(endpoint_spec, 'request_body', None) if hasattr(endpoint_spec, 'request_body') else getattr(endpoint_spec, 'req_body_other', None),
"_original_object_type": type(endpoint_spec).__name__
}
if not any(endpoint_spec_dict.values()): # 如果手动构建后仍基本为空
self.logger.error(f"手动从属性构建 endpoint_spec_dict (类型: {type(endpoint_spec)}) 后仍然为空或无效。")
endpoint_spec_dict = {} # 重置为空,触发下方错误处理
else:
try:
endpoint_spec_dict = dict(endpoint_spec)
self.logger.warning(f"直接将类型为 {type(endpoint_spec)} 的 endpoint_spec 转换为字典。这可能是一个浅拷贝,并且可能不完整。")
except TypeError:
self.logger.error(f"无法将 endpoint_spec (类型: {type(endpoint_spec)}) 转换为字典,也未找到有效的转换方法。")
endpoint_spec_dict = {}
if not endpoint_spec_dict or not endpoint_spec_dict.get("path") or endpoint_spec_dict.get("path") == 'UNKNOWN_PATH': # 如果转换后仍为空或无效
self.logger.error(f"Endpoint spec (原始类型: {type(endpoint_spec)}) 无法有效转换为包含有效路径的字典,测试用例执行可能受影响。最终 endpoint_spec_dict: {endpoint_spec_dict}")
# 创建一个最小的 endpoint_spec_dict 以允许测试用例实例化,但它将缺少大部分信息
endpoint_spec_dict = {
'method': endpoint_spec_dict.get('method', 'UNKNOWN_METHOD'), # 保留已解析的方法
'path': 'UNKNOWN_PATH_CONVERSION_FAILED',
'title': f"Unknown endpoint due to spec conversion error for original type {type(endpoint_spec)}",
'parameters': [], # 确保有空的 parameters 和 requestBody
'requestBody': None
}
# 确保 global_api_spec (应该是 ParsedSwaggerSpec 或 ParsedYAPISpec 实例) 被转换为字典
global_spec_dict: Dict[str, Any] = {}
converted_by_method: Optional[str] = None
if hasattr(global_api_spec, 'spec') and isinstance(getattr(global_api_spec, 'spec', None), dict) and getattr(global_api_spec, 'spec', None):
global_spec_dict = global_api_spec.spec # type: ignore
converted_by_method = ".spec attribute"
elif is_dataclass(global_api_spec) and not isinstance(global_api_spec, type): # Ensure it's an instance, not the class itself
try:
candidate_spec = dataclass_asdict(global_api_spec)
if isinstance(candidate_spec, dict) and candidate_spec:
global_spec_dict = candidate_spec
converted_by_method = "dataclasses.asdict()"
except Exception as e:
self.logger.debug(f"Calling dataclasses.asdict() on {type(global_api_spec)} failed: {e}, trying other methods.")
if not global_spec_dict and hasattr(global_api_spec, 'model_dump') and callable(global_api_spec.model_dump):
try:
candidate_spec = global_api_spec.model_dump()
if isinstance(candidate_spec, dict) and candidate_spec:
global_spec_dict = candidate_spec
converted_by_method = ".model_dump()"
except Exception as e:
self.logger.debug(f"Calling .model_dump() on {type(global_api_spec)} failed: {e}, trying other methods.")
if not global_spec_dict and hasattr(global_api_spec, 'dict') and callable(global_api_spec.dict):
try:
candidate_spec = global_api_spec.dict()
if isinstance(candidate_spec, dict) and candidate_spec:
global_spec_dict = candidate_spec
converted_by_method = ".dict()"
except Exception as e:
self.logger.debug(f"Calling .dict() on {type(global_api_spec)} failed: {e}, trying other methods.")
if not global_spec_dict and hasattr(global_api_spec, 'to_dict') and callable(global_api_spec.to_dict):
try:
candidate_spec = global_api_spec.to_dict()
if isinstance(candidate_spec, dict) and candidate_spec:
global_spec_dict = candidate_spec
converted_by_method = ".to_dict()"
except Exception as e:
self.logger.debug(f"Calling .to_dict() on {type(global_api_spec)} failed: {e}, trying other methods.")
if not global_spec_dict and isinstance(global_api_spec, dict) and global_api_spec:
global_spec_dict = global_api_spec
converted_by_method = "direct dict"
self.logger.warning(f"global_api_spec was already a dictionary. This might be unexpected if an object was anticipated.")
if global_spec_dict and converted_by_method:
self.logger.debug(f"Successfully converted/retrieved global_api_spec (type: {type(global_api_spec)}) to dict using {converted_by_method}.")
elif not global_spec_dict :
self.logger.error(
f"Failed to convert global_api_spec (type: {type(global_api_spec)}) to a non-empty dictionary using .spec, dataclasses.asdict(), .model_dump(), .dict(), or .to_dict(). "
f"It's also not a non-empty dictionary itself. JSON reference resolution will be severely limited or fail. Using empty global_spec_dict."
)
global_spec_dict = {}
# 将 global_spec_dict 注入到 endpoint_spec_dict 中,供可能的内部解析使用 (如果 to_dict 未包含它)
if '_global_api_spec_for_resolution' not in endpoint_spec_dict and global_spec_dict:
endpoint_spec_dict['_global_api_spec_for_resolution'] = global_spec_dict
try:
self.logger.debug(f"准备实例化测试用例类: {test_case_class.__name__} 使用 endpoint_spec (keys: {list(endpoint_spec_dict.keys()) if endpoint_spec_dict else 'None'}) 和 global_api_spec (keys: {list(global_spec_dict.keys()) if global_spec_dict else 'None'})")
test_case_instance = test_case_class(
endpoint_spec=endpoint_spec_dict,
global_api_spec=global_api_spec_dict,
json_schema_validator=self.validator # <--- 注入 JSONSchemaValidator
endpoint_spec=endpoint_spec_dict,
global_api_spec=global_spec_dict,
json_schema_validator=self.validator
)
test_case_instance.logger.info(f"开始执行测试用例 '{test_case_instance.id}' for endpoint '{endpoint_spec_dict.get('method')} {endpoint_spec_dict.get('path')}'")
self.logger.info(f"开始执行测试用例 '{test_case_instance.id}' ({test_case_instance.name}) for endpoint '{endpoint_spec_dict.get('method', 'N/A')} {endpoint_spec_dict.get('path', 'N/A')}'")
# 调用 _prepare_initial_request_data 时传递 test_case_instance
# 并直接解包返回的元组
@@ -722,26 +851,26 @@ class APITestOrchestrator:
endpoint_spec=endpoint_spec_dict
)
validation_points.extend(test_case_instance.validate_request_url(api_request_context.url, api_request_context))
validation_points.extend(test_case_instance.validate_request_headers(api_request_context.headers, api_request_context))
validation_points.extend(test_case_instance.validate_request_body(api_request_context.body, api_request_context))
validation_results.extend(test_case_instance.validate_request_url(api_request_context.url, api_request_context))
validation_results.extend(test_case_instance.validate_request_headers(api_request_context.headers, api_request_context))
validation_results.extend(test_case_instance.validate_request_body(api_request_context.body, api_request_context))
critical_pre_validation_failure = False
failure_messages = []
for vp in validation_points:
for vp in validation_results:
if not vp.passed and test_case_instance.severity in [TestSeverity.CRITICAL, TestSeverity.HIGH]: # Check severity of the Test Case for pre-validation
critical_pre_validation_failure = True
failure_messages.append(vp.message)
if critical_pre_validation_failure:
self.logger.warning(f"测试用例 '{test_case_instance.id}' 因请求预校验失败而中止 (TC严重级别: {test_case_instance.severity.value})。失败信息: {'; '.join(failure_messages)}")
tc_duration = time.time() - tc_start_time
tc_duration = time.monotonic() - start_time
return ExecutedTestCaseResult(
test_case_id=test_case_instance.id,
test_case_name=test_case_instance.name,
test_case_severity=test_case_instance.severity,
status=ExecutedTestCaseResult.Status.FAILED,
validation_points=validation_points,
validation_points=validation_results,
message=f"请求预校验失败: {'; '.join(failure_messages)}",
duration=tc_duration
)
@@ -781,32 +910,32 @@ class APITestOrchestrator:
request_context=api_request_context
)
validation_points.extend(test_case_instance.validate_response(api_response_context, api_request_context))
validation_points.extend(test_case_instance.check_performance(api_response_context, api_request_context))
validation_results.extend(test_case_instance.validate_response(api_response_context, api_request_context))
validation_results.extend(test_case_instance.check_performance(api_response_context, api_request_context))
final_status = ExecutedTestCaseResult.Status.PASSED
if any(not vp.passed for vp in validation_points):
if any(not vp.passed for vp in validation_results):
final_status = ExecutedTestCaseResult.Status.FAILED
tc_duration = time.time() - tc_start_time
tc_duration = time.monotonic() - start_time
return ExecutedTestCaseResult(
test_case_id=test_case_instance.id,
test_case_name=test_case_instance.name,
test_case_severity=test_case_instance.severity,
status=final_status,
validation_points=validation_points,
validation_points=validation_results,
duration=tc_duration
)
except Exception as e:
self.logger.error(f"执行测试用例 '{test_case_class.id if test_case_instance else test_case_class.__name__}' 时发生严重错误: {e}", exc_info=True)
tc_duration = time.time() - tc_start_time
tc_duration = time.monotonic() - start_time
return ExecutedTestCaseResult(
test_case_id=test_case_instance.id if test_case_instance else test_case_class.id if hasattr(test_case_class, 'id') else "unknown_tc_id",
test_case_name=test_case_instance.name if test_case_instance else test_case_class.name if hasattr(test_case_class, 'name') else "Unknown Test Case Name",
test_case_severity=test_case_instance.severity if test_case_instance else TestSeverity.CRITICAL,
status=ExecutedTestCaseResult.Status.ERROR,
validation_points=validation_points,
validation_points=validation_results,
message=f"测试用例执行时发生内部错误: {str(e)}",
duration=tc_duration
)