增加宽容的schema验证
This commit is contained in:
parent
f003fbbbd1
commit
39effa9461
Binary file not shown.
Binary file not shown.
@ -0,0 +1,81 @@
|
|||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext
|
||||||
|
from ddms_compliance_suite.utils.response_utils import extract_data_for_validation
|
||||||
|
from ddms_compliance_suite.utils.schema_provider import SchemaProvider
|
||||||
|
import logging
|
||||||
|
|
||||||
|
class FlexibleSchemaValidationCase(BaseAPITestCase):
|
||||||
|
"""
|
||||||
|
一个灵活的Schema验证测试用例,能够处理非标准的响应结构和动态的Schema来源。
|
||||||
|
"""
|
||||||
|
id = "TC-CORE-FUNC-002"
|
||||||
|
name = "灵活的返回体JSON Schema验证"
|
||||||
|
description = (
|
||||||
|
"验证API响应体是否符合预期的JSON Schema。此用例能够智能处理被包装的响应(如{code, data}),"
|
||||||
|
"并支持从列表响应中验证每个元素。它依赖于SchemaProvider获取schema,并设计为处理需要动态获取schema的场景。"
|
||||||
|
)
|
||||||
|
severity = TestSeverity.CRITICAL
|
||||||
|
tags = ["core-functionality", "schema-validation", "flexible"]
|
||||||
|
execution_order = 110 # 略高于标准Schema验证,以便在适用时优先执行
|
||||||
|
|
||||||
|
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)
|
||||||
|
# We need to initialize the schema_provider here, as it's no longer injected.
|
||||||
|
self.schema_provider = SchemaProvider(global_api_spec) if global_api_spec else None
|
||||||
|
self.logger.info(f"测试用例 '{self.id}' 已为端点 '{self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')}' 初始化。")
|
||||||
|
|
||||||
|
|
||||||
|
def execute(self, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||||
|
"""
|
||||||
|
执行灵活的schema验证。
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
response_context = self.api_caller.call_api(request_context)
|
||||||
|
|
||||||
|
if not response_context:
|
||||||
|
return [self.failed("API调用失败,无法获取响应进行验证。")]
|
||||||
|
|
||||||
|
# 1. 使用 SchemaProvider 获取 Schema
|
||||||
|
if not self.schema_provider:
|
||||||
|
return [self.failed("SchemaProvider 未被初始化,无法执行此测试用例。")]
|
||||||
|
|
||||||
|
expected_schema = self.schema_provider.get_schema(self.endpoint_spec, response_context.status_code)
|
||||||
|
|
||||||
|
if not expected_schema:
|
||||||
|
# 如果是成功响应但找不到schema,这可能是一个问题
|
||||||
|
if 200 <= response_context.status_code < 300:
|
||||||
|
return [self.failed(f"成功响应(状态码 {response_context.status_code}),但无法为其找到或生成JSON Schema。")]
|
||||||
|
else:
|
||||||
|
return [self.passed(f"非成功响应(状态码 {response_context.status_code})且未定义Schema,跳过验证。")]
|
||||||
|
|
||||||
|
# 2. 使用 response_utils 提取待验证的数据列表
|
||||||
|
if not response_context.json_content:
|
||||||
|
return [self.failed(f"响应内容不是有效的JSON格式,无法进行Schema验证。响应文本: {response_context.text_content[:200]}...")]
|
||||||
|
|
||||||
|
data_to_validate_list = extract_data_for_validation(response_context.json_content)
|
||||||
|
|
||||||
|
if not data_to_validate_list:
|
||||||
|
# extract_data_for_validation 在找不到数据或遇到空列表时返回空列表
|
||||||
|
return [self.passed("未从响应中提取到需要验证的数据项(可能为空列表),跳过验证。")]
|
||||||
|
|
||||||
|
# 3. 遍历列表,对每个数据项进行验证
|
||||||
|
all_items_passed = True
|
||||||
|
for i, item in enumerate(data_to_validate_list):
|
||||||
|
item_context_prefix = f"响应列表中的第 {i+1} 个元素"
|
||||||
|
validation_results = self.validate_data_against_schema(
|
||||||
|
data_to_validate=item,
|
||||||
|
schema_definition=expected_schema,
|
||||||
|
context_message_prefix=item_context_prefix
|
||||||
|
)
|
||||||
|
|
||||||
|
for res in validation_results:
|
||||||
|
if not res.passed:
|
||||||
|
all_items_passed = False
|
||||||
|
# 为错误信息添加更多上下文
|
||||||
|
res.message = f"{item_context_prefix} {res.message}"
|
||||||
|
results.append(res)
|
||||||
|
|
||||||
|
if all_items_passed:
|
||||||
|
results.append(self.passed(f"成功验证了响应中的 {len(data_to_validate_list)} 个数据项,均符合Schema。"))
|
||||||
|
|
||||||
|
return results
|
||||||
@ -1,6 +1,7 @@
|
|||||||
from typing import Dict, Any, Optional, List
|
from typing import Dict, Any, Optional, List
|
||||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
|
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
|
||||||
import json
|
import json
|
||||||
|
from ddms_compliance_suite.utils.schema_provider import SchemaProvider
|
||||||
|
|
||||||
class ResponseSchemaValidationCase(BaseAPITestCase):
|
class ResponseSchemaValidationCase(BaseAPITestCase):
|
||||||
id = "TC-CORE-FUNC-001"
|
id = "TC-CORE-FUNC-001"
|
||||||
@ -10,11 +11,10 @@ class ResponseSchemaValidationCase(BaseAPITestCase):
|
|||||||
tags = ["core-functionality", "schema-validation", "output-format"]
|
tags = ["core-functionality", "schema-validation", "output-format"]
|
||||||
execution_order = 100 # Default, can be adjusted
|
execution_order = 100 # Default, can be adjusted
|
||||||
|
|
||||||
# This test is generally applicable, especially for GET requests or successful POST/PUT.
|
|
||||||
# It might need refinement based on specific endpoint characteristics (e.g., no response body for DELETE)
|
|
||||||
|
|
||||||
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):
|
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=llm_service)
|
super().__init__(endpoint_spec, global_api_spec, json_schema_validator, llm_service=llm_service)
|
||||||
|
self.schema_provider = SchemaProvider(global_api_spec) if global_api_spec else None
|
||||||
self.logger.info(f"测试用例 '{self.id}' 已为端点 '{self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')}' 初始化。")
|
self.logger.info(f"测试用例 '{self.id}' 已为端点 '{self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')}' 初始化。")
|
||||||
|
|
||||||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||||
@ -22,37 +22,13 @@ class ResponseSchemaValidationCase(BaseAPITestCase):
|
|||||||
method = request_context.method.upper()
|
method = request_context.method.upper()
|
||||||
status_code = response_context.status_code
|
status_code = response_context.status_code
|
||||||
|
|
||||||
# Determine the expected response schema based on method and status code
|
if not self.schema_provider:
|
||||||
# This logic might need to be more sophisticated depending on how schemas are structured in your API spec (YAPI/Swagger)
|
return [self.failed("SchemaProvider 未被初始化,无法执行此测试用例。")]
|
||||||
expected_schema = None
|
|
||||||
response_spec_key = None
|
|
||||||
|
|
||||||
if 'responses' in self.endpoint_spec: # OpenAPI/Swagger style
|
|
||||||
if str(status_code) in self.endpoint_spec['responses']:
|
|
||||||
response_def = self.endpoint_spec['responses'][str(status_code)]
|
|
||||||
if 'content' in response_def and 'application/json' in response_def['content']:
|
|
||||||
expected_schema = response_def['content']['application/json'].get('schema')
|
|
||||||
response_spec_key = f"responses.{status_code}.content.application/json.schema"
|
|
||||||
elif 'default' in self.endpoint_spec['responses']: # Fallback to default response
|
|
||||||
response_def = self.endpoint_spec['responses']['default']
|
|
||||||
if 'content' in response_def and 'application/json' in response_def['content']:
|
|
||||||
expected_schema = response_def['content']['application/json'].get('schema')
|
|
||||||
response_spec_key = f"responses.default.content.application/json.schema"
|
|
||||||
elif 'res_body_type' in self.endpoint_spec and self.endpoint_spec['res_body_type'] == 'json': # YAPI style (simplified)
|
|
||||||
if 'res_body_is_json_schema' in self.endpoint_spec and self.endpoint_spec['res_body_is_json_schema']:
|
|
||||||
if self.endpoint_spec.get('res_body'):
|
|
||||||
try:
|
|
||||||
# YAPI often stores schema as a JSON string
|
|
||||||
expected_schema = json.loads(self.endpoint_spec['res_body'])
|
|
||||||
response_spec_key = "res_body (从JSON字符串解析)"
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
self.logger.error(f"从YAPI res_body解析JSON schema失败: {e}")
|
|
||||||
results.append(self.failed(f"无法从YAPI规范解析响应schema: {e}"))
|
|
||||||
return results
|
|
||||||
|
|
||||||
# Only proceed with schema validation if we have a schema and a JSON response body
|
expected_schema = self.schema_provider.get_schema(self.endpoint_spec, status_code)
|
||||||
|
|
||||||
if expected_schema and response_context.json_content is not None:
|
if expected_schema and response_context.json_content is not None:
|
||||||
self.logger.info(f"将根据路径 '{response_spec_key or '未知位置'}' 的schema验证响应体。")
|
self.logger.info(f"将根据从API规范中获取的schema验证响应体。")
|
||||||
schema_validation_results = self.validate_data_against_schema(
|
schema_validation_results = self.validate_data_against_schema(
|
||||||
data_to_validate=response_context.json_content,
|
data_to_validate=response_context.json_content,
|
||||||
schema_definition=expected_schema,
|
schema_definition=expected_schema,
|
||||||
@ -60,22 +36,18 @@ class ResponseSchemaValidationCase(BaseAPITestCase):
|
|||||||
)
|
)
|
||||||
results.extend(schema_validation_results)
|
results.extend(schema_validation_results)
|
||||||
elif response_context.json_content is None and method not in ["DELETE", "HEAD", "OPTIONS"] and status_code in [200, 201, 202]:
|
elif response_context.json_content is None and method not in ["DELETE", "HEAD", "OPTIONS"] and status_code in [200, 201, 202]:
|
||||||
# If we expected a JSON body (e.g. for successful GET/POST) but got none
|
if expected_schema:
|
||||||
if expected_schema: # and if a schema was defined
|
|
||||||
results.append(self.failed(
|
results.append(self.failed(
|
||||||
message=f"根据schema期望一个JSON响应体,但未收到可解析的JSON内容。",
|
message=f"根据schema期望一个JSON响应体,但未收到可解析的JSON内容。",
|
||||||
details={"status_code": status_code, "response_text_sample": (response_context.text_content or "")[:200]}
|
details={"status_code": status_code, "response_text_sample": (response_context.text_content or "")[:200]}
|
||||||
))
|
))
|
||||||
self.logger.warning(f"期望 {method} {request_context.url} 返回JSON响应体,但未收到或非JSON格式。")
|
self.logger.warning(f"期望 {method} {request_context.url} 返回JSON响应体,但未收到或非JSON格式。")
|
||||||
elif not expected_schema and response_context.json_content is not None and status_code // 100 == 2:
|
elif not expected_schema and response_context.json_content is not None and status_code // 100 == 2:
|
||||||
# If there is a JSON body but no schema was found for successful responses
|
|
||||||
self.logger.info(f"响应包含JSON体,但在API规范中未找到针对状态码 {status_code} 的JSON schema。跳过schema验证。")
|
self.logger.info(f"响应包含JSON体,但在API规范中未找到针对状态码 {status_code} 的JSON schema。跳过schema验证。")
|
||||||
# Optionally, add an informational validation result:
|
|
||||||
# results.append(ValidationResult(passed=True, message="Response has JSON body, but no schema defined for validation.", details={"status_code": status_code}))
|
|
||||||
elif not expected_schema and response_context.json_content is None:
|
elif not expected_schema and response_context.json_content is None:
|
||||||
self.logger.info(f"状态码 {status_code} 的响应无JSON体也无定义的schema。跳过schema验证。")
|
self.logger.info(f"状态码 {status_code} 的响应无JSON体也无定义的schema。跳过schema验证。")
|
||||||
|
|
||||||
if not results: # If no specific validation was added (e.g. schema not found but not an error)
|
if not results:
|
||||||
results.append(self.passed("Schema验证步骤完成(未发现问题,或schema不适用/未为此响应定义)。"))
|
results.append(self.passed("标准Schema验证步骤完成(未发现问题,或schema不适用/未为此响应定义)。"))
|
||||||
|
|
||||||
return results
|
return results
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -2,6 +2,7 @@ from enum import Enum
|
|||||||
from typing import Any, Dict, Optional, List, Tuple, Type, Union
|
from typing import Any, Dict, Optional, List, Tuple, Type, Union
|
||||||
import logging
|
import logging
|
||||||
from .utils import schema_utils
|
from .utils import schema_utils
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
class TestSeverity(Enum):
|
class TestSeverity(Enum):
|
||||||
"""测试用例的严重程度"""
|
"""测试用例的严重程度"""
|
||||||
@ -114,7 +115,11 @@ class BaseAPITestCase:
|
|||||||
use_llm_for_query_params: bool = False
|
use_llm_for_query_params: bool = False
|
||||||
use_llm_for_headers: bool = False
|
use_llm_for_headers: bool = False
|
||||||
|
|
||||||
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):
|
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):
|
||||||
"""
|
"""
|
||||||
初始化测试用例。
|
初始化测试用例。
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@ -42,6 +42,8 @@ except ImportError:
|
|||||||
LLMService = None
|
LLMService = None
|
||||||
logging.getLogger(__name__).info("LLMService 未找到,LLM 相关功能将不可用。")
|
logging.getLogger(__name__).info("LLMService 未找到,LLM 相关功能将不可用。")
|
||||||
|
|
||||||
|
from ddms_compliance_suite.utils.schema_provider import SchemaProvider
|
||||||
|
|
||||||
_dynamic_model_cache: Dict[str, Type[BaseModel]] = {}
|
_dynamic_model_cache: Dict[str, Type[BaseModel]] = {}
|
||||||
|
|
||||||
class ExecutedTestCaseResult:
|
class ExecutedTestCaseResult:
|
||||||
@ -510,6 +512,10 @@ class APITestOrchestrator:
|
|||||||
logging.info(f"strictness_level: {self.strictness_level}")
|
logging.info(f"strictness_level: {self.strictness_level}")
|
||||||
elif strictness_level:
|
elif strictness_level:
|
||||||
logging.warning(f"提供了无效的严格等级 '{strictness_level}'。将使用默认行为。有效值: {', '.join([e.name for e in TestSeverity])}")
|
logging.warning(f"提供了无效的严格等级 '{strictness_level}'。将使用默认行为。有效值: {', '.join([e.name for e in TestSeverity])}")
|
||||||
|
|
||||||
|
# 将这些属性的初始化移到此处,并设为None,避免在_execute_tests_from_parsed_spec之前被错误使用
|
||||||
|
self.json_schema_validator: Optional[JSONSchemaValidator] = None
|
||||||
|
self.schema_provider: Optional[SchemaProvider] = None
|
||||||
|
|
||||||
def get_api_call_details(self) -> List[APICallDetail]:
|
def get_api_call_details(self) -> List[APICallDetail]:
|
||||||
"""Returns the collected list of API call details."""
|
"""Returns the collected list of API call details."""
|
||||||
@ -981,7 +987,7 @@ class APITestOrchestrator:
|
|||||||
endpoint_spec=endpoint_spec_dict,
|
endpoint_spec=endpoint_spec_dict,
|
||||||
global_api_spec=global_spec_dict,
|
global_api_spec=global_spec_dict,
|
||||||
json_schema_validator=self.json_validator,
|
json_schema_validator=self.json_validator,
|
||||||
llm_service=self.llm_service # Pass the orchestrator's LLM service instance
|
llm_service=self.llm_service
|
||||||
)
|
)
|
||||||
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')}'")
|
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')}'")
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
38
ddms_compliance_suite/utils/response_utils.py
Normal file
38
ddms_compliance_suite/utils/response_utils.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from typing import Any, List
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def extract_data_for_validation(response_json: Any) -> List[Any]:
|
||||||
|
"""
|
||||||
|
从原始API响应JSON中智能提取需要被验证的核心业务数据列表。
|
||||||
|
即使只有一个对象,也返回一个单元素的列表。
|
||||||
|
|
||||||
|
策略:
|
||||||
|
1. 如果响应体是包含 'code' 和 'data' 的标准包装,则提取 'data' 的内容。
|
||||||
|
2. 如果处理后的数据是列表,直接返回该列表。
|
||||||
|
3. 如果处理后的数据是单个对象(字典),将其包装在单元素列表中返回。
|
||||||
|
4. 如果数据为空或不适用,返回空列表。
|
||||||
|
"""
|
||||||
|
if not response_json:
|
||||||
|
return []
|
||||||
|
|
||||||
|
data_to_process = response_json
|
||||||
|
|
||||||
|
# 策略 1: 解开标准包装
|
||||||
|
if isinstance(response_json, dict) and 'code' in response_json and 'data' in response_json:
|
||||||
|
logger.debug("检测到标准响应包装,提取 'data' 字段内容进行处理。")
|
||||||
|
data_to_process = response_json['data']
|
||||||
|
|
||||||
|
# 策略 2: 统一返回列表
|
||||||
|
if isinstance(data_to_process, list):
|
||||||
|
logger.debug(f"数据本身为列表,包含 {len(data_to_process)} 个元素,直接返回。")
|
||||||
|
return data_to_process
|
||||||
|
|
||||||
|
if isinstance(data_to_process, dict):
|
||||||
|
logger.debug("数据为单个对象,将其包装在列表中返回。")
|
||||||
|
return [data_to_process]
|
||||||
|
|
||||||
|
# 对于其他情况(如数据为None或非对象/列表类型),返回空列表
|
||||||
|
logger.warning(f"待处理的数据既不是列表也不是对象,无法提取进行验证。数据: {str(data_to_process)[:100]}")
|
||||||
|
return []
|
||||||
60
ddms_compliance_suite/utils/schema_provider.py
Normal file
60
ddms_compliance_suite/utils/schema_provider.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
class SchemaProvider:
|
||||||
|
def __init__(self, global_api_spec: Dict[str, Any]):
|
||||||
|
self.global_api_spec = global_api_spec
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def get_schema(self, endpoint_spec: Dict[str, Any], status_code: int) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
获取端点在特定状态码下的响应Schema。
|
||||||
|
|
||||||
|
当前实现:从API规范中查找。
|
||||||
|
未来可扩展:优先从动态映射中获取,如果失败或未配置,则回退到当前实现。
|
||||||
|
"""
|
||||||
|
# --- 预留的动态获取逻辑扩展点 ---
|
||||||
|
# if self._use_dynamic_provider(endpoint_spec):
|
||||||
|
# schema = self._fetch_dynamic_schema(endpoint_spec)
|
||||||
|
# if schema:
|
||||||
|
# return schema
|
||||||
|
# ---------------------------------
|
||||||
|
|
||||||
|
return self._get_schema_from_spec(endpoint_spec, status_code)
|
||||||
|
|
||||||
|
def _get_schema_from_spec(self, endpoint_spec: Dict[str, Any], status_code: int) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
(私有方法) 从API规范中提取Schema,这是当前版本的主要实现。
|
||||||
|
"""
|
||||||
|
self.logger.debug(f"尝试从API规范中为状态码 {status_code} 查找Schema。")
|
||||||
|
|
||||||
|
expected_schema = None
|
||||||
|
|
||||||
|
# 兼容 OpenAPI/Swagger 格式
|
||||||
|
if 'responses' in endpoint_spec:
|
||||||
|
responses = endpoint_spec['responses']
|
||||||
|
# 优先匹配精确的状态码
|
||||||
|
if str(status_code) in responses:
|
||||||
|
response_def = responses[str(status_code)]
|
||||||
|
if 'content' in response_def and 'application/json' in response_def['content']:
|
||||||
|
expected_schema = response_def['content']['application/json'].get('schema')
|
||||||
|
# 回退到 'default'
|
||||||
|
elif 'default' in responses:
|
||||||
|
response_def = responses['default']
|
||||||
|
if 'content' in response_def and 'application/json' in response_def['content']:
|
||||||
|
expected_schema = response_def['content']['application/json'].get('schema')
|
||||||
|
|
||||||
|
# 兼容 YAPI 格式 (简化)
|
||||||
|
elif 'res_body_type' in endpoint_spec and endpoint_spec['res_body_type'] == 'json':
|
||||||
|
if endpoint_spec.get('res_body_is_json_schema') and endpoint_spec.get('res_body'):
|
||||||
|
try:
|
||||||
|
expected_schema = json.loads(endpoint_spec['res_body'])
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
self.logger.error(f"从YAPI res_body解析JSON schema失败。res_body: {endpoint_spec['res_body']}")
|
||||||
|
return None # 解析失败
|
||||||
|
|
||||||
|
if not expected_schema:
|
||||||
|
self.logger.info(f"在API规范中未找到针对状态码 {status_code} 的JSON schema。")
|
||||||
|
|
||||||
|
return expected_schema
|
||||||
12419
log_stage.txt
12419
log_stage.txt
File diff suppressed because one or more lines are too long
49
memory-bank/refactoring_opportunities.md
Normal file
49
memory-bank/refactoring_opportunities.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# 架构重构机会
|
||||||
|
|
||||||
|
本文档记录了在开发过程中发现的、有价值的但因当前优先级或工作量而暂未实施的架构重构机会。旨在为未来的代码质量提升和迭代开发提供清晰的路线图。
|
||||||
|
|
||||||
|
## 1. 改进 `APITestOrchestrator` 的依赖注入和生命周期管理
|
||||||
|
|
||||||
|
### 当前问题与"不优雅"的根源
|
||||||
|
|
||||||
|
在为测试用例引入 `SchemaProvider` 作为核心依赖时,我们遇到了一个典型的 **对象生命周期与依赖注入时序问题**。
|
||||||
|
|
||||||
|
1. **理想的设计** 是由框架(`APITestOrchestrator`)在初始化时创建并持有一个全局唯一的 `SchemaProvider` 实例,然后在创建测试用例时,将这个实例 **注入** 进去。
|
||||||
|
2. **现实的障碍** 是 `SchemaProvider` 的创建依赖于 `global_api_spec`(即解析后的API规范文件),而 `global_api_spec` 本身是在 `APITestOrchestrator` 实例化很久之后,在 `run_tests_from_yapi/swagger` 等方法内部才被加载和创建的。
|
||||||
|
3. **导致的结果** 是我们无法在 `APITestOrchestrator` 的 `__init__` 方法中优雅地创建 `SchemaProvider`,这导致了 `AttributeError`。
|
||||||
|
|
||||||
|
我们当前采用的临时方案是让每个测试用例自己在 `__init__` 中创建 `SchemaProvider` 实例。这虽然解决了功能问题,但存在代码重复,并且违背了依赖注入的最佳实践。
|
||||||
|
|
||||||
|
### 待实施的重构方案
|
||||||
|
|
||||||
|
#### 方案A: 内部逻辑提炼 (中期优化)
|
||||||
|
|
||||||
|
这是我们讨论过的、侵入性较小的优化方案。
|
||||||
|
|
||||||
|
1. 在 `APITestOrchestrator` 中创建一个新的私有方法,例如 `_initialize_spec_dependent_services(self, parsed_spec)`。
|
||||||
|
2. 此方法负责获取 `parsed_spec`,设置 `self.global_api_spec`,并创建所有依赖它的服务(如 `SchemaProvider`),将实例存放在 `self` 的属性中。
|
||||||
|
3. 在 `run_tests_from_yapi` 和 `run_tests_from_swagger` 的开头,调用这个新的私有方法。
|
||||||
|
4. 改造 `BaseAPITestCase`,使其能够接收注入的 `schema_provider` 实例。
|
||||||
|
|
||||||
|
**优点**: 实现简单,风险小,能有效减少测试用例中的代码重复。
|
||||||
|
|
||||||
|
#### 方案B: 彻底分离业务阶段 (长期目标)
|
||||||
|
|
||||||
|
这是更理想、更彻底的重构方案,它将 `APITestOrchestrator` 的功能进行明确的阶段划分。
|
||||||
|
|
||||||
|
1. **重构 `APITestOrchestrator` 的公共API**,将其从一个大而全的 `run_tests_from_...` 方法,拆分为多个独立的、职责单一的公共方法,例如:
|
||||||
|
* `load_spec(file_path: str) -> ParsedAPISpec`: 只负责加载和解析文件,返回一个解析后的对象。
|
||||||
|
* `prepare_execution(parsed_spec: ParsedAPISpec)`: 接收解析后的对象,用它来初始化所有依赖的服务,如 `SchemaProvider`,并准备好待测试的端点列表。
|
||||||
|
* `execute() -> TestSummary`: 执行所有准备好的测试,并返回最终的摘要。
|
||||||
|
|
||||||
|
2. **调整调用流程**: `run_api_tests.py` 中的 `main` 函数也需要相应调整,以适配这种分阶段的调用方式。
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
* **完全符合单一职责原则**:每个方法只做一件事。
|
||||||
|
* **生命周期清晰**:数据和依赖的生命周期管理变得非常清晰、可控。
|
||||||
|
* **高度可测试**:可以单独对 `load_spec` 或 `prepare_execution` 进行单元测试。
|
||||||
|
* **更灵活**: 未来可以支持更复杂的场景,比如加载多个API规范文件后合并测试。
|
||||||
|
|
||||||
|
### 重构收益
|
||||||
|
|
||||||
|
完成上述任一方案(特别是方案B),将使得测试框架的核心代码更加健壮、可维护和可扩展,为未来添加更复杂的功能(如测试依赖、多文件聚合测试等)打下坚实的基础。
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user