""" 测试编排器模块 负责组合API解析器、API调用器、验证器和规则执行器,进行端到端的API测试 """ import logging import json import time from typing import Dict, List, Any, Optional, Union, Tuple from enum import Enum import datetime from .input_parser.parser import InputParser, YAPIEndpoint, SwaggerEndpoint, ParsedYAPISpec, ParsedSwaggerSpec from .api_caller.caller import APICaller, APIRequest, APIResponse from .json_schema_validator.validator import JSONSchemaValidator from .rule_repository.repository import RuleRepository from .rule_executor.executor import RuleExecutor from .models.rule_models import RuleQuery, TargetType, RuleCategory, RuleLifecycle, RuleScope from .models.config_models import RuleRepositoryConfig, RuleStorageConfig class TestResult: """测试结果类""" class Status(str, Enum): """测试状态枚举""" PASSED = "通过" FAILED = "失败" ERROR = "错误" SKIPPED = "跳过" def __init__(self, endpoint_id: str, endpoint_name: str, status: Status, message: str = "", api_request: Optional[APIRequest] = None, api_response: Optional[APIResponse] = None, validation_details: Optional[Dict[str, Any]] = None, elapsed_time: float = 0.0): """ 初始化测试结果 Args: endpoint_id: API端点ID(通常是方法+路径的组合) endpoint_name: API端点名称 status: 测试状态 message: 测试结果消息 api_request: API请求对象 api_response: API响应对象 validation_details: 验证详情 elapsed_time: 执行耗时(秒) """ self.endpoint_id = endpoint_id self.endpoint_name = endpoint_name self.status = status self.message = message self.api_request = api_request self.api_response = api_response self.validation_details = validation_details or {} self.elapsed_time = elapsed_time self.timestamp = datetime.datetime.now() def to_dict(self) -> Dict[str, Any]: """将测试结果转换为字典""" result = { "endpoint_id": self.endpoint_id, "endpoint_name": self.endpoint_name, "status": self.status, "message": self.message, "elapsed_time": self.elapsed_time, "timestamp": self.timestamp.isoformat(), } if self.api_request: result["api_request"] = { "method": self.api_request.method, "url": str(self.api_request.url), "params": self.api_request.params, "body": self.api_request.json_data } if self.api_response: result["api_response"] = { "status_code": self.api_response.status_code, "content": self.api_response.json_content if self.api_response.json_content else str(self.api_response.content), "elapsed_time": self.api_response.elapsed_time } if self.validation_details: result["validation_details"] = self.validation_details return result class TestSummary: """测试结果摘要""" def __init__(self): """初始化测试结果摘要""" self.total = 0 self.passed = 0 self.failed = 0 self.error = 0 self.skipped = 0 self.start_time = datetime.datetime.now() self.end_time: Optional[datetime.datetime] = None self.results: List[TestResult] = [] def add_result(self, result: TestResult): """添加测试结果""" self.total += 1 if result.status == TestResult.Status.PASSED: self.passed += 1 elif result.status == TestResult.Status.FAILED: self.failed += 1 elif result.status == TestResult.Status.ERROR: self.error += 1 elif result.status == TestResult.Status.SKIPPED: self.skipped += 1 self.results.append(result) def finalize(self): """完成测试,记录结束时间""" self.end_time = datetime.datetime.now() @property def duration(self) -> float: """测试持续时间(秒)""" if not self.end_time: return 0.0 return (self.end_time - self.start_time).total_seconds() @property def success_rate(self) -> float: """测试成功率""" if self.total == 0: return 0.0 return self.passed / self.total * 100 def to_dict(self) -> Dict[str, Any]: """将测试结果摘要转换为字典""" return { "total": self.total, "passed": self.passed, "failed": self.failed, "error": self.error, "skipped": self.skipped, "success_rate": f"{self.success_rate:.2f}%", "start_time": self.start_time.isoformat(), "end_time": self.end_time.isoformat() if self.end_time else None, "duration": f"{self.duration:.2f}秒", "results": [result.to_dict() for result in self.results] } def to_json(self, pretty=True) -> str: """将测试结果摘要转换为JSON字符串""" indent = 2 if pretty else None return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False) def print_summary(self): """打印测试结果摘要""" print(f"\n测试结果摘要:") print(f"总测试数: {self.total}") print(f"通过: {self.passed}") print(f"失败: {self.failed}") print(f"错误: {self.error}") print(f"跳过: {self.skipped}") print(f"成功率: {self.success_rate:.2f}%") print(f"总耗时: {self.duration:.2f}秒") class APITestOrchestrator: """API测试编排器""" def __init__(self, base_url: str, rule_repo_path: str = "./rules"): """ 初始化API测试编排器 Args: base_url: API基础URL rule_repo_path: 规则库路径 """ self.base_url = base_url.rstrip('/') self.logger = logging.getLogger(__name__) # 初始化组件 self.parser = InputParser() self.api_caller = APICaller() self.validator = JSONSchemaValidator() # 初始化规则库和规则执行器 rule_config = RuleRepositoryConfig( storage=RuleStorageConfig(path=rule_repo_path) ) self.rule_repo = RuleRepository(rule_config) self.rule_executor = RuleExecutor(self.rule_repo) def _build_api_request(self, endpoint: Union[YAPIEndpoint, SwaggerEndpoint]) -> Tuple[APIRequest, Dict[str, Any]]: """ 构建API请求对象 Args: endpoint: API端点对象 Returns: Tuple[APIRequest, Dict[str, Any]]: API请求对象和测试数据 """ # 获取端点信息 if hasattr(endpoint, 'method'): method = endpoint.method else: method = "GET" # 默认方法 if hasattr(endpoint, 'path'): path = endpoint.path else: path = "/" # 默认路径 # 替换路径中的参数占位符 path_params = {} if "{" in path and "}" in path: # 查找路径中的所有参数 import re param_matches = re.findall(r'\{([^}]+)\}', path) for param in param_matches: # 生成一个随机值作为参数 path_params[param] = f"test_{param}" # 查找请求参数 params = {} headers = {"Content-Type": "application/json", "Accept": "application/json"} body = None # YAPI端点特有属性 if hasattr(endpoint, 'req_headers') and endpoint.req_headers: for header in endpoint.req_headers: if 'name' in header and 'value' in header: headers[header['name']] = header['value'] if hasattr(endpoint, 'req_query') and endpoint.req_query: for query in endpoint.req_query: if 'name' in query: params[query['name']] = query.get('value', '') if hasattr(endpoint, 'req_body_type') and endpoint.req_body_type == 'json' and hasattr(endpoint, 'req_body_other'): try: # 如果req_body_other是JSON字符串,它可能包含请求体schema req_body_schema = json.loads(endpoint.req_body_other) if isinstance(endpoint.req_body_other, str) else None if req_body_schema and isinstance(req_body_schema, dict): # 使用schema生成请求体 body = self._generate_data_from_schema(req_body_schema) else: # 如果不是有效的schema,使用它作为请求体示例 body = req_body_schema except json.JSONDecodeError: # 如果解析失败,使用一个默认的请求体 self.logger.warning(f"无法解析YAPI请求体JSON: {endpoint.req_body_other}") body = {"test": "data"} # Swagger端点特有属性 if hasattr(endpoint, 'parameters') and endpoint.parameters: for param in endpoint.parameters: param_in = param.get('in', '') param_name = param.get('name', '') param_schema = param.get('schema', {}) if not param_name: continue # 生成参数值,优先使用example param_value = param.get('example', None) if param_value is None and param_schema: param_value = self._generate_data_from_schema(param_schema) if param_value is None: # 使用默认值 param_value = param.get('default', 'test_value') if param_in == 'query': params[param_name] = param_value elif param_in == 'header': headers[param_name] = str(param_value) elif param_in == 'path' and param_name in path_params: path_params[param_name] = str(param_value) if hasattr(endpoint, 'request_body') and endpoint.request_body: content = endpoint.request_body.get('content', {}) json_content = content.get('application/json', {}) if 'example' in json_content: body = json_content['example'] elif 'schema' in json_content: # 基于schema创建请求体 body = self._generate_data_from_schema(json_content['schema']) # 构建完整URL(替换路径参数) url = self.base_url + path for param, value in path_params.items(): url = url.replace(f"{{{param}}}", str(value)) # 创建API请求 request = None try: request = APIRequest( method=method, url=url, headers=headers, params=params, json_data=body ) except Exception as e: self.logger.error(f"创建API请求时发生错误: {e}") raise e # 执行请求准备阶段的规则 endpoint_id = f"{method.upper()} {path}" # 创建规则执行上下文 context = { 'api_request': request, 'endpoint_id': endpoint_id, 'endpoint': endpoint, 'path_params': path_params, 'query_params': params, 'headers': headers, 'body': body } # 执行请求准备阶段的规则 rule_results = self.rule_executor.execute_rules_for_lifecycle( lifecycle=RuleLifecycle.REQUEST_PREPARATION, context=context ) # 保存规则执行结果,以便在测试结果中使用 self.last_request_rule_results = rule_results # 保存请求对象,以便在响应验证时使用 self.last_request = request # 收集测试数据 test_data = { "path_params": path_params, "query_params": params, "headers": headers, "body": body, "rule_results": [ { "rule_id": result.rule.id, "rule_name": result.rule.name, "is_valid": result.is_valid, "message": result.message } for result in rule_results ] } return request, test_data def _validate_response(self, response: APIResponse, endpoint: Union[YAPIEndpoint, SwaggerEndpoint]) -> Dict[str, Any]: """ 验证API响应 Args: response: API响应对象 endpoint: API端点对象 Returns: Dict[str, Any]: 验证结果 """ validation_results = { "status_code": { "is_valid": 200 <= response.status_code < 300, "expected": "2XX", "actual": response.status_code }, "json_format": { "is_valid": response.json_content is not None, "message": "响应应为有效的JSON格式" if response.json_content is None else "响应是有效的JSON格式" } } # 尝试从API定义中提取响应schema进行验证 schema = None schema_source = "未知" # 从YAPI定义中提取响应schema if hasattr(endpoint, 'res_body') and endpoint.res_body and response.json_content: try: # YAPI中的res_body通常是JSON字符串格式的schema if isinstance(endpoint.res_body, str) and endpoint.res_body.strip(): schema = json.loads(endpoint.res_body) schema_source = "YAPI响应定义" except json.JSONDecodeError: self.logger.warning(f"无法解析YAPI响应schema: {endpoint.res_body}") # 从Swagger定义中提取响应schema elif hasattr(endpoint, 'responses') and endpoint.responses and response.json_content: # Swagger中通常以状态码为key,包含schema定义 success_responses = endpoint.responses.get('200', {}) or endpoint.responses.get('201', {}) if not success_responses and any(str(k).startswith('2') for k in endpoint.responses.keys()): # 尝试查找任何2xx响应 for k in endpoint.responses.keys(): if str(k).startswith('2'): success_responses = endpoint.responses[k] break if success_responses: schema_obj = None if 'schema' in success_responses: schema_obj = success_responses['schema'] elif 'content' in success_responses and 'application/json' in success_responses['content']: schema_obj = success_responses['content']['application/json'].get('schema') if schema_obj: schema = schema_obj schema_source = "Swagger响应定义" # 使用提取的schema进行验证 if schema and response.json_content: try: result = self.validator.validate(response.json_content, schema) validation_results["schema_validation"] = { "source": schema_source, "is_valid": result.is_valid, "errors": result.errors if not result.is_valid else [] } except Exception as e: self.logger.error(f"验证响应时发生错误: {str(e)}") validation_results["schema_validation"] = { "source": schema_source, "is_valid": False, "errors": [f"验证过程中发生错误: {str(e)}"] } # 如果我们有JSON Schema规则,可以验证响应体 endpoint_id = "" if hasattr(endpoint, 'path'): path = endpoint.path method = getattr(endpoint, 'method', "GET") endpoint_id = f"{method.upper()} {path}" schema_rules = self.rule_repo.get_rules_for_target( target_type=TargetType.API_RESPONSE, target_id=endpoint_id ) if schema_rules: # 使用找到的第一个规则 from .models.rule_models import JSONSchemaDefinition for schema_rule in schema_rules: if isinstance(schema_rule, JSONSchemaDefinition): # 验证响应体 if response.json_content: result = self.validator.validate_with_rule(response.json_content, schema_rule) validation_results[f"rule_schema_validation_{schema_rule.id}"] = { "source": f"规则库 ({schema_rule.id})", "is_valid": result.is_valid, "errors": result.errors if not result.is_valid else [] } # 使用规则执行器验证规则 # 创建执行上下文 if hasattr(endpoint, 'path'): api_request = None if hasattr(self, 'last_request'): api_request = self.last_request context = { 'api_response': response, 'api_request': api_request, 'endpoint_id': endpoint_id, 'endpoint': endpoint } # 执行响应验证阶段的规则 rule_results = self.rule_executor.execute_rules_for_lifecycle( lifecycle=RuleLifecycle.RESPONSE_VALIDATION, context=context ) # 将规则执行结果添加到验证结果中 for i, rule_result in enumerate(rule_results): validation_results[f"rule_execution_{i}"] = { "rule_id": rule_result.rule.id, "rule_name": rule_result.rule.name, "is_valid": rule_result.is_valid, "message": rule_result.message, "details": rule_result.details } # 基本验证: 检查返回码、响应时间等 validation_results["response_time"] = { "value": response.elapsed_time, "message": f"响应时间: {response.elapsed_time:.4f}秒" } return validation_results def run_test_for_endpoint(self, endpoint: Union[YAPIEndpoint, SwaggerEndpoint]) -> TestResult: """ 运行单个API端点的测试 Args: endpoint: API端点对象 Returns: TestResult: 测试结果 """ # 获取端点信息 endpoint_id = f"{getattr(endpoint, 'method', 'GET')} {getattr(endpoint, 'path', '/')}" endpoint_name = getattr(endpoint, 'title', '') or getattr(endpoint, 'summary', '') or endpoint_id self.logger.info(f"测试端点: {endpoint_id} - {endpoint_name}") try: # 构建API请求 request, test_data = self._build_api_request(endpoint) # 检查请求准备阶段的规则验证结果 request_rule_failures = [] for rule_result in test_data.get("rule_results", []): if not rule_result.get("is_valid", True): request_rule_failures.append(f"{rule_result.get('rule_name', '未知规则')}: {rule_result.get('message', '验证失败')}") # 如果有关键性的请求验证失败,可以选择跳过API调用 if request_rule_failures and any("严重错误" in failure for failure in request_rule_failures): return TestResult( endpoint_id=endpoint_id, endpoint_name=endpoint_name, status=TestResult.Status.FAILED, message=f"请求准备阶段验证失败: {'; '.join(request_rule_failures)}", api_request=request, api_response=None, validation_details={"request_rule_failures": request_rule_failures}, elapsed_time=0.0 ) # 发送请求 start_time = time.time() response = self.api_caller.call_api(request) elapsed_time = time.time() - start_time # 验证响应 validation_results = self._validate_response(response, endpoint) # 执行请求后处理规则 context = { 'api_request': request, 'api_response': response, 'endpoint_id': endpoint_id, 'endpoint': endpoint, 'elapsed_time': elapsed_time } post_rule_results = self.rule_executor.execute_rules_for_lifecycle( lifecycle=RuleLifecycle.POST_VALIDATION, context=context ) # 将后处理规则结果添加到验证结果中 for i, rule_result in enumerate(post_rule_results): validation_results[f"post_rule_execution_{i}"] = { "rule_id": rule_result.rule.id, "rule_name": rule_result.rule.name, "is_valid": rule_result.is_valid, "message": rule_result.message, "details": rule_result.details } # 判断测试是否通过 # 检查所有验证结果是否有失败的 rule_failures = [] validation_failures = [] for key, result in validation_results.items(): if isinstance(result, dict) and 'is_valid' in result and not result['is_valid']: if key.startswith('rule_execution_') or key.startswith('post_rule_execution_'): rule_name = result.get('rule_name', '未知规则') rule_message = result.get('message', '验证失败') rule_failures.append(f"{rule_name}: {rule_message}") else: validation_failures.append(result.get('message', f"{key}验证失败")) # 合并请求规则失败和响应规则失败 all_rule_failures = request_rule_failures + rule_failures # 决定测试结果状态 if not validation_failures and not all_rule_failures: # 所有验证和规则都通过 result = TestResult( endpoint_id=endpoint_id, endpoint_name=endpoint_name, status=TestResult.Status.PASSED, message="API测试通过", api_request=request, api_response=response, validation_details=validation_results, elapsed_time=elapsed_time ) elif not validation_failures and all_rule_failures: # 基本验证通过,但规则验证失败 result = TestResult( endpoint_id=endpoint_id, endpoint_name=endpoint_name, status=TestResult.Status.FAILED, message=f"API规则验证失败: {'; '.join(all_rule_failures)}", api_request=request, api_response=response, validation_details=validation_results, elapsed_time=elapsed_time ) self.logger.error(f"接口{endpoint_id} 规则验证失败: {'; '.join(all_rule_failures)}") else: # 基本验证失败 result = TestResult( endpoint_id=endpoint_id, endpoint_name=endpoint_name, status=TestResult.Status.FAILED, message=f"API测试失败: {'; '.join(validation_failures)}", api_request=request, api_response=response, validation_details=validation_results, elapsed_time=elapsed_time ) self.logger.error(f"接口{endpoint_id} 测试失败: {'; '.join(validation_failures)}") return result except Exception as e: self.logger.error(f"测试端点 {endpoint_id} 时发生错误: {str(e)}") return TestResult( endpoint_id=endpoint_id, endpoint_name=endpoint_name, status=TestResult.Status.ERROR, message=f"测试执行错误: {str(e)}", elapsed_time=0.0 ) def run_tests_from_yapi(self, yapi_file_path: str, categories: Optional[List[str]] = None) -> TestSummary: """ 从YAPI定义文件运行API测试 Args: yapi_file_path: YAPI定义文件路径 categories: 要测试的API分类列表(如果为None,则测试所有分类) Returns: TestSummary: 测试结果摘要 """ # 解析YAPI文件 self.logger.info(f"从YAPI文件加载API定义: {yapi_file_path}") parsed_yapi = self.parser.parse_yapi_spec(yapi_file_path) if not parsed_yapi: self.logger.error(f"解析YAPI文件失败: {yapi_file_path}") # 创建一个空的测试摘要 summary = TestSummary() summary.finalize() return summary # 筛选端点 endpoints = parsed_yapi.endpoints if categories: endpoints = [endpoint for endpoint in endpoints if endpoint.category_name in categories] # 运行测试 summary = TestSummary() for endpoint in endpoints: result = self.run_test_for_endpoint(endpoint) summary.add_result(result) summary.finalize() return summary def run_tests_from_swagger(self, swagger_file_path: str, tags: Optional[List[str]] = None) -> TestSummary: """ 从Swagger定义文件运行API测试 Args: swagger_file_path: Swagger定义文件路径 tags: 要测试的API标签列表(如果为None,则测试所有标签) Returns: TestSummary: 测试结果摘要 """ # 解析Swagger文件 self.logger.info(f"从Swagger文件加载API定义: {swagger_file_path}") parsed_swagger = self.parser.parse_swagger_spec(swagger_file_path) if not parsed_swagger: self.logger.error(f"解析Swagger文件失败: {swagger_file_path}") # 创建一个空的测试摘要 summary = TestSummary() summary.finalize() return summary # 筛选端点 endpoints = parsed_swagger.endpoints if tags: endpoints = [endpoint for endpoint in endpoints if any(tag in endpoint.tags for tag in tags)] # 运行测试 summary = TestSummary() for endpoint in endpoints: result = self.run_test_for_endpoint(endpoint) summary.add_result(result) summary.finalize() return summary def _generate_data_from_schema(self, schema: Dict[str, Any]) -> Any: """ 根据JSON Schema生成测试数据 Args: schema: JSON Schema Returns: 生成的测试数据 """ if not schema: return None schema_type = schema.get('type') if schema_type == 'object': result = {} properties = schema.get('properties', {}) for prop_name, prop_schema in properties.items(): # 首先检查是否有example或default值 if 'example' in prop_schema: result[prop_name] = prop_schema['example'] elif 'default' in prop_schema: result[prop_name] = prop_schema['default'] else: # 递归生成子属性的值 result[prop_name] = self._generate_data_from_schema(prop_schema) return result elif schema_type == 'array': # 为数组生成一个样本项 items_schema = schema.get('items', {}) # 默认生成1个元素,对于测试来说通常足够 return [self._generate_data_from_schema(items_schema)] elif schema_type == 'string': # 处理不同的字符串格式 string_format = schema.get('format', '') if string_format == 'date': return '2023-01-01' elif string_format == 'date-time': return '2023-01-01T12:00:00Z' elif string_format == 'email': return 'test@example.com' elif string_format == 'uuid': return '00000000-0000-0000-0000-000000000000' elif 'enum' in schema: # 如果有枚举值,选择第一个 return schema['enum'][0] if schema['enum'] else 'enum_value' elif 'pattern' in schema: # 如果有正则表达式模式,返回一个简单的符合模式的字符串 # 注意:这里只是一个简单处理,不能处理所有正则表达式 return f"pattern_{schema['pattern']}_value" else: return 'test_string' elif schema_type == 'number' or schema_type == 'integer': # 处理数值类型 if 'minimum' in schema and 'maximum' in schema: # 如果有最小值和最大值,取中间值 return (schema['minimum'] + schema['maximum']) / 2 elif 'minimum' in schema: return schema['minimum'] elif 'maximum' in schema: return schema['maximum'] elif schema_type == 'integer': return 1 else: return 1.0 elif schema_type == 'boolean': return True elif schema_type == 'null': return None # 如果是复杂类型或未知类型,返回一个默认值 return 'test_value' # python run_api_tests.py --base-url http://127.0.0.1:4523/m1/6386850-6083489-default --yapi assets/doc/井筒API示例.json