feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
# Everything is Suitable API Test
|
||||
|
||||
黑盒API测试工具,用于对Everything is Suitable API系统执行发布前的质量评估与验证。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 测试数据管理:支持JSON、CSV等多种格式的测试用例与测试数据存储
|
||||
- 内存数据存储:使用内存数据结构管理测试数据,无需数据库
|
||||
- API测试功能:支持RESTful API的所有HTTP方法(GET、POST、PUT、DELETE等)
|
||||
- 请求构造:支持请求参数构造、请求头配置、认证授权处理
|
||||
- 响应验证:支持状态码检查、响应体断言、响应时间阈值验证
|
||||
- 依赖处理:支持API依赖关系处理与测试用例执行顺序控制
|
||||
- 测试报告:支持HTML和JSON格式的测试报告,包含可视化图表
|
||||
- 命令行接口:提供完整的命令行接口,支持测试套件指定和过滤
|
||||
- 日志记录:实现详细的日志记录系统
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Python 3.10+
|
||||
- Poetry(依赖管理)
|
||||
- requests/httpx(HTTP客户端)
|
||||
- pytest(测试框架)
|
||||
- Jinja2(模板引擎)
|
||||
- PyYAML(配置文件解析)
|
||||
- Click(命令行框架)
|
||||
|
||||
## 安装
|
||||
|
||||
### 前置要求
|
||||
|
||||
- Python 3.10+
|
||||
- Poetry 1.6.0+
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. 克隆项目
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd api-test-tool
|
||||
```
|
||||
|
||||
2. 安装依赖
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
|
||||
3. 配置环境变量(可选)
|
||||
```bash
|
||||
export TEST_USERNAME=admin
|
||||
export TEST_PASSWORD=admin123
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
### 基本命令
|
||||
|
||||
```bash
|
||||
# 查看版本
|
||||
apitest --version
|
||||
|
||||
# 查看帮助
|
||||
apitest --help
|
||||
|
||||
# 运行所有测试
|
||||
apitest run --suite all
|
||||
|
||||
# 运行指定模块的测试
|
||||
apitest run --suite user
|
||||
|
||||
# 运行高优先级的测试用例
|
||||
apitest run --suite all --filter priority=high
|
||||
|
||||
# 并发执行测试
|
||||
apitest run --suite all --parallel --threads 4
|
||||
|
||||
# 列出所有测试用例
|
||||
apitest list --suite all
|
||||
|
||||
# 生成测试报告
|
||||
apitest report --format html --output reports/test.html
|
||||
|
||||
# 设置配置
|
||||
apitest config --set test.parallel=true
|
||||
|
||||
# 获取配置
|
||||
apitest config --get test.parallel
|
||||
|
||||
# 验证配置
|
||||
apitest config --validate
|
||||
```
|
||||
|
||||
### 配置文件
|
||||
|
||||
配置文件位于 `config/config.yaml`,包含以下配置项:
|
||||
|
||||
- **target**: 目标系统配置(base_url、timeout、max_retries)
|
||||
- **auth**: 认证配置(login_endpoint、username、password、token_storage)
|
||||
- **test**: 测试配置(data_dir、test_cases_dir、parallel、retry_count)
|
||||
- **report**: 报告配置(output_dir、formats、include_details)
|
||||
- **logging**: 日志配置(level、file、format、console)
|
||||
- **data**: 数据管理配置(load_on_startup、auto_refresh、cache_enabled)
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
api-test-tool/
|
||||
├── src/apitest/ # 源代码
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── client/ # HTTP客户端
|
||||
│ ├── utils/ # 工具类
|
||||
│ ├── config/ # 配置管理
|
||||
│ ├── core/ # 核心功能
|
||||
│ ├── data/ # 数据管理
|
||||
│ ├── report/ # 报告生成
|
||||
│ └── cli/ # 命令行接口
|
||||
├── data/ # 测试数据
|
||||
├── test_cases/ # 测试用例
|
||||
├── reports/ # 测试报告
|
||||
├── logs/ # 日志文件
|
||||
├── config/ # 配置文件
|
||||
├── tests/ # 测试代码
|
||||
│ ├── unit/ # 单元测试
|
||||
│ └── integration/ # 集成测试
|
||||
├── examples/ # 示例代码
|
||||
├── docs/ # 文档
|
||||
│ ├── api/ # API文档
|
||||
│ ├── user/ # 用户手册
|
||||
│ └── developer/ # 开发指南
|
||||
├── pyproject.toml # Poetry配置
|
||||
├── README.md # 项目说明
|
||||
└── .gitignore # Git忽略文件
|
||||
```
|
||||
|
||||
## 开发
|
||||
|
||||
### 代码规范
|
||||
|
||||
- 遵循PEP 8代码规范
|
||||
- 使用类型注解
|
||||
- 使用Google风格文档字符串
|
||||
- 代码格式化使用black
|
||||
- 代码检查使用flake8
|
||||
- 类型检查使用mypy
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
poetry run pytest
|
||||
|
||||
# 运行单元测试
|
||||
poetry run pytest tests/unit/
|
||||
|
||||
# 运行集成测试
|
||||
poetry run pytest tests/integration/
|
||||
|
||||
# 生成覆盖率报告
|
||||
poetry run pytest --cov=apitest --cov-report=html
|
||||
```
|
||||
|
||||
### 代码格式化
|
||||
|
||||
```bash
|
||||
# 格式化代码
|
||||
poetry run black src/ tests/
|
||||
|
||||
# 检查代码风格
|
||||
poetry run flake8 src/ tests/
|
||||
|
||||
# 类型检查
|
||||
poetry run mypy src/
|
||||
```
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎贡献代码!请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 了解贡献指南。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 联系方式
|
||||
|
||||
- 项目主页: <repository-url>
|
||||
- 问题反馈: <issues-url>
|
||||
- 邮箱: test@example.com
|
||||
@@ -0,0 +1,45 @@
|
||||
# 目标系统配置
|
||||
target:
|
||||
base_url: http://localhost:8080
|
||||
timeout: 5000
|
||||
max_retries: 3
|
||||
|
||||
# 认证配置
|
||||
auth:
|
||||
login_endpoint: /sys/auth/login
|
||||
username: ${TEST_USERNAME:admin}
|
||||
password: ${TEST_PASSWORD:admin123}
|
||||
token_storage: memory
|
||||
token_refresh: true
|
||||
|
||||
# 测试配置
|
||||
test:
|
||||
data_dir: data
|
||||
test_cases_dir: test_cases
|
||||
parallel: false
|
||||
parallel_threads: 4
|
||||
retry_count: 2
|
||||
stop_on_failure: false
|
||||
max_response_time: 5000
|
||||
|
||||
# 报告配置
|
||||
report:
|
||||
output_dir: reports
|
||||
formats:
|
||||
- html
|
||||
- json
|
||||
include_details: true
|
||||
include_charts: true
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level: INFO
|
||||
file: logs/api-test.log
|
||||
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
console: true
|
||||
|
||||
# 数据管理配置
|
||||
data:
|
||||
load_on_startup: true
|
||||
auto_refresh: false
|
||||
cache_enabled: true
|
||||
@@ -0,0 +1,48 @@
|
||||
[tool.poetry]
|
||||
name = "everything-is-suitable-api-test"
|
||||
version = "1.0.0"
|
||||
description = "黑盒API测试工具"
|
||||
authors = ["Test Team <test@example.com>"]
|
||||
readme = "README.md"
|
||||
packages = [{include = "apitest", from = "src"}]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
requests = "^2.31.0"
|
||||
httpx = "^0.25.0"
|
||||
pytest = "^7.4.0"
|
||||
allure-pytest = "^2.13.2"
|
||||
pyyaml = "^6.0.1"
|
||||
python-dotenv = "^1.0.0"
|
||||
click = "^8.1.6"
|
||||
jinja2 = "^3.1.2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest-cov = "^4.1.0"
|
||||
black = "^23.12.0"
|
||||
flake8 = "^6.1.0"
|
||||
mypy = "^1.7.0"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
apitest = "apitest.cli_module:cli"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ['py310']
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.10"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
python_classes = ["Test*"]
|
||||
python_functions = ["test_*"]
|
||||
addopts = "-v --cov=apitest --cov-report=html --cov-report=term"
|
||||
@@ -0,0 +1,15 @@
|
||||
# 核心依赖
|
||||
requests==2.31.0
|
||||
httpx==0.25.0
|
||||
pytest==7.4.0
|
||||
allure-pytest==2.13.2
|
||||
pyyaml==6.0.1
|
||||
python-dotenv==1.0.0
|
||||
click==8.1.6
|
||||
jinja2==3.1.2
|
||||
|
||||
# 开发依赖
|
||||
pytest-cov==4.1.0
|
||||
black==23.12.0
|
||||
flake8==6.1.0
|
||||
mypy==1.7.0
|
||||
@@ -0,0 +1,35 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="everything-is-suitable-api-test",
|
||||
version="1.0.0",
|
||||
description="黑盒API测试工具",
|
||||
author="Test Team",
|
||||
author_email="test@example.com",
|
||||
packages=find_packages(where="src"),
|
||||
package_dir={"": "src"},
|
||||
install_requires=[
|
||||
"requests>=2.31.0",
|
||||
"httpx>=0.25.0",
|
||||
"pytest>=7.4.0",
|
||||
"allure-pytest>=2.13.2",
|
||||
"pyyaml>=6.0.1",
|
||||
"python-dotenv>=1.0.0",
|
||||
"click>=8.1.6",
|
||||
"jinja2>=3.1.2",
|
||||
],
|
||||
extras_require={
|
||||
"dev": [
|
||||
"pytest-cov>=4.1.0",
|
||||
"black>=23.12.0",
|
||||
"flake8>=6.1.0",
|
||||
"mypy>=1.7.0",
|
||||
],
|
||||
},
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"apitest=apitest.main:cli",
|
||||
],
|
||||
},
|
||||
python_requires=">=3.10",
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
__version__ = "1.0.0"
|
||||
__author__ = "Test Team"
|
||||
__email__ = "test@example.com"
|
||||
@@ -0,0 +1,5 @@
|
||||
"""CLI模块"""
|
||||
|
||||
from apitest.cli_module import cli
|
||||
|
||||
__all__ = ["cli"]
|
||||
@@ -0,0 +1,223 @@
|
||||
"""CLI命令行接口"""
|
||||
|
||||
import click
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from apitest.config.config_manager import ConfigManager
|
||||
from apitest.config.logger_manager import LoggerManager
|
||||
from apitest.orchestrator.test_orchestrator import TestOrchestrator
|
||||
from apitest.data.test_data_manager import TestDataManager
|
||||
from apitest.models.test_models import HTTPMethod
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version="1.0.0")
|
||||
def cli():
|
||||
"""黑盒API测试工具 - 命令行接口"""
|
||||
pass
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--test-cases", "-t", type=click.Path(exists=True), help="测试用例文件路径(JSON格式)")
|
||||
@click.option("--test-data", "-d", type=click.Path(exists=True), help="测试数据文件路径(CSV格式)")
|
||||
@click.option("--module", "-m", help="按模块过滤测试用例")
|
||||
@click.option("--tag", help="按标签过滤测试用例")
|
||||
@click.option("--priority", type=int, help="按优先级过滤测试用例")
|
||||
@click.option("--stop-on-failure", is_flag=True, help="在失败时停止执行")
|
||||
@click.option("--no-report", is_flag=True, help="不生成测试报告")
|
||||
@click.option("--report-format", type=click.Choice(["html", "json"]), default="html", help="报告格式")
|
||||
@click.option("--report-path", type=click.Path(), help="报告输出路径")
|
||||
@click.option("--verbose", "-v", is_flag=True, help="详细输出")
|
||||
def run(
|
||||
test_cases: Optional[str],
|
||||
test_data: Optional[str],
|
||||
module: Optional[str],
|
||||
tag: Optional[str],
|
||||
priority: Optional[int],
|
||||
stop_on_failure: bool,
|
||||
no_report: bool,
|
||||
report_format: str,
|
||||
report_path: Optional[str],
|
||||
verbose: bool
|
||||
):
|
||||
"""运行测试用例"""
|
||||
try:
|
||||
config_manager = ConfigManager()
|
||||
logger_manager = LoggerManager(config_manager)
|
||||
logger = logger_manager.get_logger(__name__)
|
||||
|
||||
if verbose:
|
||||
logger.info("详细模式已启用")
|
||||
|
||||
orchestrator = TestOrchestrator(config_manager, logger_manager)
|
||||
data_manager = TestDataManager(logger)
|
||||
|
||||
if not test_cases:
|
||||
click.echo("错误: 请指定测试用例文件路径 (--test-cases)", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
test_cases_path = Path(test_cases)
|
||||
cases = data_manager.load_test_cases_from_json(test_cases_path)
|
||||
|
||||
if test_data:
|
||||
test_data_path = Path(test_data)
|
||||
data = data_manager.load_test_data_from_csv(test_data_path)
|
||||
|
||||
if len(cases) == 1:
|
||||
cases = data_manager.parameterize_test_case(cases[0], data)
|
||||
else:
|
||||
logger.warning("多个测试用例不支持参数化,测试数据将被忽略")
|
||||
|
||||
filtered_cases = _filter_test_cases(cases, module, tag, priority)
|
||||
|
||||
if len(filtered_cases) == 0:
|
||||
click.echo("警告: 没有匹配的测试用例")
|
||||
sys.exit(0)
|
||||
|
||||
logger.info(f"开始执行 {len(filtered_cases)} 个测试用例")
|
||||
|
||||
result = orchestrator.run_test_suite(
|
||||
filtered_cases,
|
||||
stop_on_failure=stop_on_failure,
|
||||
generate_report=not no_report,
|
||||
report_format=report_format,
|
||||
report_path=Path(report_path) if report_path else None
|
||||
)
|
||||
|
||||
logger.info(f"测试完成: 通过 {result.passed}, 失败 {result.failed}, 跳过 {result.skipped}")
|
||||
logger.info(f"通过率: {result.pass_rate:.2f}%")
|
||||
logger.info(f"执行时长: {result.duration:.2f}秒")
|
||||
|
||||
sys.exit(0 if result.failed == 0 else 1)
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"执行测试时出错: {e}", err=True)
|
||||
if verbose:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("test-cases", type=click.Path(exists=True))
|
||||
@click.option("--module", "-m", help="按模块过滤")
|
||||
@click.option("--tag", help="按标签过滤")
|
||||
@click.option("--priority", type=int, help="按优先级过滤")
|
||||
def list(test_cases: str, module: Optional[str], tag: Optional[str], priority: Optional[int]):
|
||||
"""列出测试用例"""
|
||||
try:
|
||||
config_manager = ConfigManager()
|
||||
logger_manager = LoggerManager(config_manager)
|
||||
logger = logger_manager.get_logger(__name__)
|
||||
|
||||
data_manager = TestDataManager(logger)
|
||||
cases = data_manager.load_test_cases_from_json(Path(test_cases))
|
||||
|
||||
filtered_cases = _filter_test_cases(cases, module, tag, priority)
|
||||
|
||||
click.echo(f"\n测试用例总数: {len(filtered_cases)}\n")
|
||||
|
||||
for case in filtered_cases:
|
||||
status = "✓" if case.enabled else "✗"
|
||||
click.echo(f"{status} {case.id}: {case.name}")
|
||||
click.echo(f" 模块: {case.module}")
|
||||
click.echo(f" 方法: {case.method.value} {case.endpoint}")
|
||||
click.echo(f" 优先级: {case.priority}")
|
||||
if case.tags:
|
||||
click.echo(f" 标签: {', '.join(case.tags)}")
|
||||
if case.dependencies:
|
||||
click.echo(f" 依赖: {', '.join(case.dependencies)}")
|
||||
click.echo()
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"列出测试用例时出错: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("test-cases", type=click.Path(exists=True))
|
||||
@click.option("--output", "-o", type=click.Path(), help="输出文件路径")
|
||||
def validate(test_cases: str, output: Optional[str]):
|
||||
"""验证测试用例文件"""
|
||||
try:
|
||||
config_manager = ConfigManager()
|
||||
logger_manager = LoggerManager(config_manager)
|
||||
logger = logger_manager.get_logger(__name__)
|
||||
|
||||
data_manager = TestDataManager(logger)
|
||||
cases = data_manager.load_test_cases_from_json(Path(test_cases))
|
||||
|
||||
click.echo(f"验证测试用例文件: {test_cases}")
|
||||
click.echo(f"测试用例数量: {len(cases)}")
|
||||
|
||||
errors = []
|
||||
for i, case in enumerate(cases):
|
||||
if not case.id:
|
||||
errors.append(f"测试用例 {i+1}: 缺少ID")
|
||||
if not case.name:
|
||||
errors.append(f"测试用例 {i+1}: 缺少名称")
|
||||
if not case.endpoint:
|
||||
errors.append(f"测试用例 {i+1}: 缺少端点")
|
||||
if not case.method:
|
||||
errors.append(f"测试用例 {i+1}: 缺少HTTP方法")
|
||||
|
||||
if errors:
|
||||
click.echo("\n验证失败:")
|
||||
for error in errors:
|
||||
click.echo(f" - {error}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
click.echo("\n验证通过 ✓")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"验证测试用例时出错: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--key", "-k", help="配置键")
|
||||
def config(key: Optional[str]):
|
||||
"""查看配置"""
|
||||
try:
|
||||
config_manager = ConfigManager()
|
||||
|
||||
if key:
|
||||
value = config_manager.get(key)
|
||||
click.echo(f"{key} = {value}")
|
||||
else:
|
||||
click.echo("当前配置:")
|
||||
click.echo(f" 基础URL: {config_manager.get_base_url()}")
|
||||
click.echo(f" 超时时间: {config_manager.get_timeout()}秒")
|
||||
click.echo(f" 日志级别: {config_manager.get_log_level()}")
|
||||
click.echo(f" 日志文件: {config_manager.get_log_file()}")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"查看配置时出错: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _filter_test_cases(
|
||||
cases: list,
|
||||
module: Optional[str],
|
||||
tag: Optional[str],
|
||||
priority: Optional[int]
|
||||
) -> list:
|
||||
"""过滤测试用例"""
|
||||
filtered = cases
|
||||
|
||||
if module:
|
||||
filtered = [c for c in filtered if c.module == module]
|
||||
|
||||
if tag:
|
||||
filtered = [c for c in filtered if tag in c.tags]
|
||||
|
||||
if priority is not None:
|
||||
filtered = [c for c in filtered if c.priority == priority]
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
@@ -0,0 +1,4 @@
|
||||
from .api_client import APIClient
|
||||
from .auth_manager import AuthManager
|
||||
|
||||
__all__ = ["APIClient", "AuthManager"]
|
||||
@@ -0,0 +1,306 @@
|
||||
import time
|
||||
from typing import Optional, Dict, Any, Union
|
||||
from datetime import datetime
|
||||
import requests
|
||||
from apitest.models.test_models import HTTPMethod, PerformanceMetrics
|
||||
from apitest.models.exceptions import RequestException
|
||||
|
||||
|
||||
class APIClient:
|
||||
"""API客户端"""
|
||||
|
||||
def __init__(self, base_url: str, timeout: int = 5000, max_retries: int = 3, logger=None):
|
||||
"""初始化API客户端
|
||||
|
||||
Args:
|
||||
base_url: 基础URL
|
||||
timeout: 超时时间(毫秒)
|
||||
max_retries: 最大重试次数
|
||||
logger: 日志记录器
|
||||
"""
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.timeout = timeout / 1000
|
||||
self.max_retries = max_retries
|
||||
self.logger = logger
|
||||
self._session = requests.Session()
|
||||
self._default_headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
|
||||
def set_default_headers(self, headers: Dict[str, str]):
|
||||
"""设置默认请求头
|
||||
|
||||
Args:
|
||||
headers: 请求头字典
|
||||
"""
|
||||
self._default_headers.update(headers)
|
||||
|
||||
def set_auth_token(self, token: str):
|
||||
"""设置认证token
|
||||
|
||||
Args:
|
||||
token: 认证token
|
||||
"""
|
||||
self._default_headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
def _build_url(self, endpoint: str) -> str:
|
||||
"""构建完整URL
|
||||
|
||||
Args:
|
||||
endpoint: API端点
|
||||
|
||||
Returns:
|
||||
完整URL
|
||||
"""
|
||||
endpoint = endpoint.lstrip("/")
|
||||
return f"{self.base_url}/{endpoint}"
|
||||
|
||||
def _merge_headers(self, headers: Optional[Dict[str, str]]) -> Dict[str, str]:
|
||||
"""合并请求头
|
||||
|
||||
Args:
|
||||
headers: 请求头字典
|
||||
|
||||
Returns:
|
||||
合并后的请求头
|
||||
"""
|
||||
merged = self._default_headers.copy()
|
||||
if headers:
|
||||
merged.update(headers)
|
||||
return merged
|
||||
|
||||
def _calculate_metrics(
|
||||
self,
|
||||
start_time: float,
|
||||
request_data: Union[Dict, str, None],
|
||||
response_data: Any
|
||||
) -> PerformanceMetrics:
|
||||
"""计算性能指标
|
||||
|
||||
Args:
|
||||
start_time: 请求开始时间
|
||||
request_data: 请求数据
|
||||
response_data: 响应数据
|
||||
|
||||
Returns:
|
||||
性能指标
|
||||
"""
|
||||
end_time = time.time()
|
||||
response_time = int((end_time - start_time) * 1000)
|
||||
|
||||
request_size = 0
|
||||
if request_data:
|
||||
if isinstance(request_data, dict):
|
||||
request_size = len(str(request_data))
|
||||
elif isinstance(request_data, str):
|
||||
request_size = len(request_data)
|
||||
|
||||
response_size = 0
|
||||
if response_data:
|
||||
response_size = len(str(response_data))
|
||||
|
||||
return PerformanceMetrics(
|
||||
response_time=response_time,
|
||||
request_size=request_size,
|
||||
response_size=response_size,
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
|
||||
def _execute_request(
|
||||
self,
|
||||
method: HTTPMethod,
|
||||
url: str,
|
||||
headers: Dict[str, str],
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
body: Optional[Dict[str, Any]] = None
|
||||
) -> requests.Response:
|
||||
"""执行HTTP请求
|
||||
|
||||
Args:
|
||||
method: HTTP方法
|
||||
url: 请求URL
|
||||
headers: 请求头
|
||||
params: URL参数
|
||||
body: 请求体
|
||||
|
||||
Returns:
|
||||
响应对象
|
||||
|
||||
Raises:
|
||||
RequestException: 请求失败时抛出
|
||||
"""
|
||||
try:
|
||||
if method == HTTPMethod.GET:
|
||||
return self._session.get(url, headers=headers, params=params, timeout=self.timeout)
|
||||
elif method == HTTPMethod.POST:
|
||||
return self._session.post(url, headers=headers, params=params, json=body, timeout=self.timeout)
|
||||
elif method == HTTPMethod.PUT:
|
||||
return self._session.put(url, headers=headers, params=params, json=body, timeout=self.timeout)
|
||||
elif method == HTTPMethod.DELETE:
|
||||
return self._session.delete(url, headers=headers, params=params, timeout=self.timeout)
|
||||
elif method == HTTPMethod.PATCH:
|
||||
return self._session.patch(url, headers=headers, params=params, json=body, timeout=self.timeout)
|
||||
elif method == HTTPMethod.HEAD:
|
||||
return self._session.head(url, headers=headers, params=params, timeout=self.timeout)
|
||||
elif method == HTTPMethod.OPTIONS:
|
||||
return self._session.options(url, headers=headers, params=params, timeout=self.timeout)
|
||||
else:
|
||||
raise RequestException(f"不支持的HTTP方法: {method}")
|
||||
|
||||
except requests.Timeout:
|
||||
raise RequestException(f"请求超时: {url}")
|
||||
except requests.ConnectionError:
|
||||
raise RequestException(f"连接失败: {url}")
|
||||
except requests.RequestException as e:
|
||||
raise RequestException(f"请求异常: {e}")
|
||||
|
||||
def request(
|
||||
self,
|
||||
method: HTTPMethod,
|
||||
endpoint: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
body: Optional[Dict[str, Any]] = None,
|
||||
retry_count: int = 0
|
||||
) -> Dict[str, Any]:
|
||||
"""发送HTTP请求
|
||||
|
||||
Args:
|
||||
method: HTTP方法
|
||||
endpoint: API端点
|
||||
headers: 请求头
|
||||
params: URL参数
|
||||
body: 请求体
|
||||
retry_count: 当前重试次数
|
||||
|
||||
Returns:
|
||||
包含响应数据和性能指标的字典
|
||||
|
||||
Raises:
|
||||
RequestException: 请求失败且重试次数用尽时抛出
|
||||
"""
|
||||
url = self._build_url(endpoint)
|
||||
merged_headers = self._merge_headers(headers)
|
||||
|
||||
if self.logger:
|
||||
self.logger.debug(f"发送{method.value}请求: {url}")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
response = self._execute_request(method, url, merged_headers, params, body)
|
||||
|
||||
try:
|
||||
response_body = response.json()
|
||||
except ValueError:
|
||||
response_body = response.text
|
||||
|
||||
performance = self._calculate_metrics(start_time, body, response_body)
|
||||
|
||||
if self.logger:
|
||||
self.logger.debug(
|
||||
f"响应: HTTP {response.status_code}, "
|
||||
f"耗时: {performance.response_time}ms, "
|
||||
f"大小: {performance.response_size}字节"
|
||||
)
|
||||
|
||||
return {
|
||||
"status_code": response.status_code,
|
||||
"response_body": response_body,
|
||||
"response_headers": dict(response.headers),
|
||||
"performance": performance
|
||||
}
|
||||
|
||||
except RequestException as e:
|
||||
if retry_count < self.max_retries:
|
||||
if self.logger:
|
||||
self.logger.warning(f"请求失败,正在重试 ({retry_count + 1}/{self.max_retries}): {e}")
|
||||
time.sleep(1 * (retry_count + 1))
|
||||
return self.request(method, endpoint, headers, params, body, retry_count + 1)
|
||||
else:
|
||||
if self.logger:
|
||||
self.logger.error(f"请求失败,重试次数用尽: {e}")
|
||||
raise
|
||||
|
||||
def get(
|
||||
self,
|
||||
endpoint: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
params: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送GET请求
|
||||
|
||||
Args:
|
||||
endpoint: API端点
|
||||
headers: 请求头
|
||||
params: URL参数
|
||||
|
||||
Returns:
|
||||
响应数据
|
||||
"""
|
||||
return self.request(HTTPMethod.GET, endpoint, headers, params)
|
||||
|
||||
def post(
|
||||
self,
|
||||
endpoint: str,
|
||||
body: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
params: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送POST请求
|
||||
|
||||
Args:
|
||||
endpoint: API端点
|
||||
body: 请求体
|
||||
headers: 请求头
|
||||
params: URL参数
|
||||
|
||||
Returns:
|
||||
响应数据
|
||||
"""
|
||||
return self.request(HTTPMethod.POST, endpoint, headers, params, body)
|
||||
|
||||
def put(
|
||||
self,
|
||||
endpoint: str,
|
||||
body: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
params: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送PUT请求
|
||||
|
||||
Args:
|
||||
endpoint: API端点
|
||||
body: 请求体
|
||||
headers: 请求头
|
||||
params: URL参数
|
||||
|
||||
Returns:
|
||||
响应数据
|
||||
"""
|
||||
return self.request(HTTPMethod.PUT, endpoint, headers, params, body)
|
||||
|
||||
def delete(
|
||||
self,
|
||||
endpoint: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
params: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送DELETE请求
|
||||
|
||||
Args:
|
||||
endpoint: API端点
|
||||
headers: 请求头
|
||||
params: URL参数
|
||||
|
||||
Returns:
|
||||
响应数据
|
||||
"""
|
||||
return self.request(HTTPMethod.DELETE, endpoint, headers, params)
|
||||
|
||||
def close(self):
|
||||
"""关闭会话"""
|
||||
self._session.close()
|
||||
if self.logger:
|
||||
self.logger.debug("API客户端会话已关闭")
|
||||
@@ -0,0 +1,201 @@
|
||||
import json
|
||||
import time
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from apitest.models.exceptions import AuthException
|
||||
|
||||
|
||||
class AuthManager:
|
||||
"""认证管理器"""
|
||||
|
||||
def __init__(self, base_url: str, credentials: Dict[str, str], logger):
|
||||
"""初始化认证管理器
|
||||
|
||||
Args:
|
||||
base_url: 基础URL
|
||||
credentials: 认证凭据
|
||||
logger: 日志记录器
|
||||
"""
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.credentials = credentials
|
||||
self.logger = logger
|
||||
self._token: Optional[str] = None
|
||||
self._token_expiry: Optional[datetime] = None
|
||||
self._refresh_token: Optional[str] = None
|
||||
self._login_endpoint: str = "/sys/auth/login"
|
||||
|
||||
def set_login_endpoint(self, endpoint: str):
|
||||
"""设置登录端点
|
||||
|
||||
Args:
|
||||
endpoint: 登录端点
|
||||
"""
|
||||
self._login_endpoint = endpoint
|
||||
|
||||
def login(self, login_endpoint: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""执行登录操作
|
||||
|
||||
Args:
|
||||
login_endpoint: 登录端点,默认使用配置的端点
|
||||
|
||||
Returns:
|
||||
登录响应数据
|
||||
|
||||
Raises:
|
||||
AuthException: 登录失败时抛出
|
||||
"""
|
||||
endpoint = login_endpoint or self._login_endpoint
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
|
||||
self.logger.info(f"尝试登录: {url}")
|
||||
|
||||
try:
|
||||
import requests
|
||||
|
||||
response = requests.post(
|
||||
url,
|
||||
json={
|
||||
"username": self.credentials.get("username", ""),
|
||||
"password": self.credentials.get("password", "")
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
if "data" in data and "token" in data["data"]:
|
||||
self._token = data["data"]["token"]
|
||||
self._refresh_token = data["data"].get("refreshToken")
|
||||
|
||||
expiry_seconds = data["data"].get("expiresIn", 3600)
|
||||
self._token_expiry = datetime.now() + timedelta(seconds=expiry_seconds)
|
||||
|
||||
self.logger.info("登录成功")
|
||||
return data
|
||||
else:
|
||||
raise AuthException("登录响应中未找到token")
|
||||
else:
|
||||
raise AuthException(f"登录失败: HTTP {response.status_code}")
|
||||
|
||||
except requests.RequestException as e:
|
||||
raise AuthException(f"登录请求失败: {e}")
|
||||
|
||||
def get_token(self) -> Optional[str]:
|
||||
"""获取当前token
|
||||
|
||||
Returns:
|
||||
当前token,如果未登录则返回None
|
||||
"""
|
||||
return self._token
|
||||
|
||||
def set_token(self, token: str, expiry_seconds: int = 3600):
|
||||
"""设置token
|
||||
|
||||
Args:
|
||||
token: 认证令牌
|
||||
expiry_seconds: 过期时间(秒),默认3600秒
|
||||
"""
|
||||
self._token = token
|
||||
self._token_expiry = datetime.now() + timedelta(seconds=expiry_seconds)
|
||||
self.logger.info("Token已设置")
|
||||
|
||||
def is_token_valid(self) -> bool:
|
||||
"""检查token是否有效
|
||||
|
||||
Returns:
|
||||
token是否有效
|
||||
"""
|
||||
if not self._token or not self._token_expiry:
|
||||
return False
|
||||
|
||||
return datetime.now() < self._token_expiry
|
||||
|
||||
def refresh_token(self) -> bool:
|
||||
"""刷新token
|
||||
|
||||
Returns:
|
||||
刷新是否成功
|
||||
"""
|
||||
if not self._refresh_token:
|
||||
self.logger.warning("没有可用的refresh token")
|
||||
return False
|
||||
|
||||
try:
|
||||
import requests
|
||||
|
||||
url = f"{self.base_url}/sys/auth/refresh"
|
||||
response = requests.post(
|
||||
url,
|
||||
json={"refreshToken": self._refresh_token},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
if "data" in data and "token" in data["data"]:
|
||||
self._token = data["data"]["token"]
|
||||
self._refresh_token = data["data"].get("refreshToken")
|
||||
|
||||
expiry_seconds = data["data"].get("expiresIn", 3600)
|
||||
self._token_expiry = datetime.now() + timedelta(seconds=expiry_seconds)
|
||||
|
||||
self.logger.info("Token刷新成功")
|
||||
return True
|
||||
|
||||
self.logger.warning("Token刷新失败,尝试重新登录")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Token刷新异常: {e}")
|
||||
return False
|
||||
|
||||
def ensure_authenticated(self) -> str:
|
||||
"""确保已认证,如果token过期则自动刷新或重新登录
|
||||
|
||||
Returns:
|
||||
有效的token
|
||||
|
||||
Raises:
|
||||
AuthException: 认证失败时抛出
|
||||
"""
|
||||
if not self._token:
|
||||
self.login()
|
||||
elif not self.is_token_valid():
|
||||
if not self.refresh_token():
|
||||
self.login()
|
||||
|
||||
if not self._token:
|
||||
raise AuthException("无法获取有效的认证token")
|
||||
|
||||
return self._token
|
||||
|
||||
def logout(self):
|
||||
"""登出"""
|
||||
self._token = None
|
||||
self._refresh_token = None
|
||||
self._token_expiry = None
|
||||
self.logger.info("已登出")
|
||||
|
||||
def get_auth_headers(self) -> Dict[str, str]:
|
||||
"""获取认证请求头
|
||||
|
||||
Returns:
|
||||
包含认证信息的请求头字典
|
||||
"""
|
||||
token = self.ensure_authenticated()
|
||||
return {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
def set_credentials(self, username: str, password: str):
|
||||
"""设置认证凭据
|
||||
|
||||
Args:
|
||||
username: 用户名
|
||||
password: 密码
|
||||
"""
|
||||
self.credentials = {"username": username, "password": password}
|
||||
self.logger.info("认证凭据已更新")
|
||||
@@ -0,0 +1,4 @@
|
||||
from .config_manager import ConfigManager
|
||||
from .logger_manager import LoggerManager, setup_logger
|
||||
|
||||
__all__ = ["ConfigManager", "LoggerManager", "setup_logger"]
|
||||
@@ -0,0 +1,175 @@
|
||||
import os
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
from dotenv import load_dotenv
|
||||
from apitest.models.exceptions import ConfigException
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""配置管理器"""
|
||||
|
||||
def __init__(self, config_path: Optional[str] = None):
|
||||
"""初始化配置管理器
|
||||
|
||||
Args:
|
||||
config_path: 配置文件路径,默认为项目根目录的config/config.yaml
|
||||
"""
|
||||
if config_path is None:
|
||||
project_root = Path(__file__).parent.parent.parent.parent
|
||||
config_path = project_root / "config" / "config.yaml"
|
||||
|
||||
self.config_path = Path(config_path)
|
||||
self._config: Dict[str, Any] = {}
|
||||
self._load_config()
|
||||
self._load_env_vars()
|
||||
|
||||
def _load_config(self):
|
||||
"""加载配置文件"""
|
||||
if not self.config_path.exists():
|
||||
raise ConfigException(f"配置文件不存在: {self.config_path}")
|
||||
|
||||
try:
|
||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||
self._config = yaml.safe_load(f) or {}
|
||||
except yaml.YAMLError as e:
|
||||
raise ConfigException(f"配置文件解析失败: {e}")
|
||||
except Exception as e:
|
||||
raise ConfigException(f"加载配置文件失败: {e}")
|
||||
|
||||
def _load_env_vars(self):
|
||||
"""加载环境变量"""
|
||||
env_file = Path(__file__).parent.parent.parent / ".env"
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file)
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""获取配置值
|
||||
|
||||
Args:
|
||||
key: 配置键,支持点号分隔的嵌套键(如:target.base_url)
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
配置值
|
||||
"""
|
||||
keys = key.split(".")
|
||||
value = self._config
|
||||
|
||||
for k in keys:
|
||||
if isinstance(value, dict) and k in value:
|
||||
value = value[k]
|
||||
else:
|
||||
return default
|
||||
|
||||
return value
|
||||
|
||||
def get_target_config(self) -> Dict[str, Any]:
|
||||
"""获取目标系统配置"""
|
||||
return self.get("target", {})
|
||||
|
||||
def get_auth_config(self) -> Dict[str, Any]:
|
||||
"""获取认证配置"""
|
||||
return self.get("auth", {})
|
||||
|
||||
def get_test_config(self) -> Dict[str, Any]:
|
||||
"""获取测试配置"""
|
||||
return self.get("test", {})
|
||||
|
||||
def get_report_config(self) -> Dict[str, Any]:
|
||||
"""获取报告配置"""
|
||||
return self.get("report", {})
|
||||
|
||||
def get_logging_config(self) -> Dict[str, Any]:
|
||||
"""获取日志配置"""
|
||||
return self.get("logging", {})
|
||||
|
||||
def get_data_config(self) -> Dict[str, Any]:
|
||||
"""获取数据配置"""
|
||||
return self.get("data", {})
|
||||
|
||||
def get_base_url(self) -> str:
|
||||
"""获取基础URL"""
|
||||
return self.get("target.base_url", "")
|
||||
|
||||
def get_timeout(self) -> int:
|
||||
"""获取超时时间(毫秒)"""
|
||||
return self.get("target.timeout", 5000)
|
||||
|
||||
def get_max_retries(self) -> int:
|
||||
"""获取最大重试次数"""
|
||||
return self.get("target.max_retries", 3)
|
||||
|
||||
def get_auth_credentials(self) -> Dict[str, str]:
|
||||
"""获取认证凭据"""
|
||||
auth_config = self.get_auth_config()
|
||||
username = os.getenv("TEST_USERNAME", auth_config.get("username", ""))
|
||||
password = os.getenv("TEST_PASSWORD", auth_config.get("password", ""))
|
||||
return {"username": username, "password": password}
|
||||
|
||||
def get_login_endpoint(self) -> str:
|
||||
"""获取登录端点"""
|
||||
return self.get("auth.login_endpoint", "/sys/auth/login")
|
||||
|
||||
def get_data_dir(self) -> Path:
|
||||
"""获取数据目录"""
|
||||
data_dir = self.get("test.data_dir", "data")
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
return project_root / data_dir
|
||||
|
||||
def get_test_cases_dir(self) -> Path:
|
||||
"""获取测试用例目录"""
|
||||
test_cases_dir = self.get("test.test_cases_dir", "test_cases")
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
return project_root / test_cases_dir
|
||||
|
||||
def get_report_dir(self) -> Path:
|
||||
"""获取报告目录"""
|
||||
report_dir = self.get("report.output_dir", "reports")
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
return project_root / report_dir
|
||||
|
||||
def is_parallel_enabled(self) -> bool:
|
||||
"""是否启用并行执行"""
|
||||
return self.get("test.parallel", False)
|
||||
|
||||
def get_parallel_threads(self) -> int:
|
||||
"""获取并行线程数"""
|
||||
return self.get("test.parallel_threads", 4)
|
||||
|
||||
def get_retry_count(self) -> int:
|
||||
"""获取重试次数"""
|
||||
return self.get("test.retry_count", 2)
|
||||
|
||||
def should_stop_on_failure(self) -> bool:
|
||||
"""是否在失败时停止"""
|
||||
return self.get("test.stop_on_failure", False)
|
||||
|
||||
def get_max_response_time(self) -> int:
|
||||
"""获取最大响应时间(毫秒)"""
|
||||
return self.get("test.max_response_time", 5000)
|
||||
|
||||
def get_report_format(self) -> str:
|
||||
"""获取报告格式"""
|
||||
return self.get("report.format", "html")
|
||||
|
||||
def get_log_level(self) -> str:
|
||||
"""获取日志级别"""
|
||||
return self.get("logging.level", "INFO")
|
||||
|
||||
def get_log_format(self) -> str:
|
||||
"""获取日志格式"""
|
||||
return self.get("logging.format", "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
|
||||
def get_log_file(self) -> Optional[Path]:
|
||||
"""获取日志文件路径"""
|
||||
log_file = self.get("logging.file")
|
||||
if log_file:
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
return project_root / log_file
|
||||
return None
|
||||
|
||||
def reload(self):
|
||||
"""重新加载配置"""
|
||||
self._load_config()
|
||||
self._load_env_vars()
|
||||
@@ -0,0 +1,103 @@
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from apitest.config.config_manager import ConfigManager
|
||||
|
||||
|
||||
class LoggerManager:
|
||||
"""日志管理器"""
|
||||
|
||||
def __init__(self, config_manager: ConfigManager):
|
||||
"""初始化日志管理器
|
||||
|
||||
Args:
|
||||
config_manager: 配置管理器实例
|
||||
"""
|
||||
self.config_manager = config_manager
|
||||
self._loggers: dict = {}
|
||||
self._setup_root_logger()
|
||||
|
||||
def _setup_root_logger(self):
|
||||
"""设置根日志记录器"""
|
||||
log_level = self.config_manager.get_log_level()
|
||||
log_format = self.config_manager.get_log_format()
|
||||
log_file = self.config_manager.get_log_file()
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
|
||||
|
||||
formatter = logging.Formatter(log_format)
|
||||
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
if log_file:
|
||||
log_file_path = Path(log_file)
|
||||
log_file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
file_handler = logging.FileHandler(log_file_path, encoding="utf-8")
|
||||
file_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(file_handler)
|
||||
|
||||
def get_logger(self, name: str) -> logging.Logger:
|
||||
"""获取日志记录器
|
||||
|
||||
Args:
|
||||
name: 日志记录器名称
|
||||
|
||||
Returns:
|
||||
日志记录器实例
|
||||
"""
|
||||
if name not in self._loggers:
|
||||
self._loggers[name] = logging.getLogger(name)
|
||||
return self._loggers[name]
|
||||
|
||||
def set_level(self, level: str):
|
||||
"""设置日志级别
|
||||
|
||||
Args:
|
||||
level: 日志级别(DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
"""
|
||||
log_level = getattr(logging, level.upper(), logging.INFO)
|
||||
logging.getLogger().setLevel(log_level)
|
||||
|
||||
def add_file_handler(self, file_path: Path, level: Optional[str] = None):
|
||||
"""添加文件处理器
|
||||
|
||||
Args:
|
||||
file_path: 日志文件路径
|
||||
level: 日志级别
|
||||
"""
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
handler = logging.FileHandler(file_path, encoding="utf-8")
|
||||
|
||||
log_format = self.config_manager.get_log_format()
|
||||
formatter = logging.Formatter(log_format)
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
if level:
|
||||
handler.setLevel(getattr(logging, level.upper(), logging.INFO))
|
||||
|
||||
logging.getLogger().addHandler(handler)
|
||||
|
||||
def remove_all_handlers(self):
|
||||
"""移除所有处理器"""
|
||||
root_logger = logging.getLogger()
|
||||
for handler in root_logger.handlers[:]:
|
||||
root_logger.removeHandler(handler)
|
||||
handler.close()
|
||||
|
||||
|
||||
def setup_logger(config_manager: ConfigManager) -> LoggerManager:
|
||||
"""设置日志系统
|
||||
|
||||
Args:
|
||||
config_manager: 配置管理器实例
|
||||
|
||||
Returns:
|
||||
日志管理器实例
|
||||
"""
|
||||
return LoggerManager(config_manager)
|
||||
@@ -0,0 +1,4 @@
|
||||
from .test_engine import TestEngine
|
||||
from .validation_engine import ValidationEngine
|
||||
|
||||
__all__ = ["TestEngine", "ValidationEngine"]
|
||||
@@ -0,0 +1,400 @@
|
||||
from typing import List, Dict, Any, Optional
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from apitest.models.test_models import (
|
||||
TestCase, TestResult, TestSuiteResult, HTTPMethod, PerformanceMetrics
|
||||
)
|
||||
from apitest.client.api_client import APIClient
|
||||
from apitest.client.auth_manager import AuthManager
|
||||
from apitest.core.validation_engine import ValidationEngine
|
||||
from apitest.models.exceptions import TestRunException, RequestException, ValidationException
|
||||
|
||||
|
||||
class TestEngine:
|
||||
"""测试引擎"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_client: APIClient,
|
||||
auth_manager: Optional[AuthManager] = None,
|
||||
validation_engine: Optional[ValidationEngine] = None,
|
||||
logger=None
|
||||
):
|
||||
"""初始化测试引擎
|
||||
|
||||
Args:
|
||||
api_client: API客户端
|
||||
auth_manager: 认证管理器
|
||||
validation_engine: 验证引擎
|
||||
logger: 日志记录器
|
||||
"""
|
||||
self.api_client = api_client
|
||||
self.auth_manager = auth_manager
|
||||
self.validation_engine = validation_engine or ValidationEngine(logger)
|
||||
self.logger = logger
|
||||
self._context: Dict[str, Any] = {}
|
||||
self._dependency_map: Dict[str, List[str]] = defaultdict(list)
|
||||
self._reverse_dependency_map: Dict[str, List[str]] = defaultdict(list)
|
||||
|
||||
def set_context(self, key: str, value: Any):
|
||||
"""设置上下文变量
|
||||
|
||||
Args:
|
||||
key: 键
|
||||
value: 值
|
||||
"""
|
||||
self._context[key] = value
|
||||
if self.logger:
|
||||
self.logger.debug(f"设置上下文变量: {key}")
|
||||
|
||||
def get_context(self, key: str, default: Any = None) -> Any:
|
||||
"""获取上下文变量
|
||||
|
||||
Args:
|
||||
key: 键
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
值
|
||||
"""
|
||||
return self._context.get(key, default)
|
||||
|
||||
def _build_dependency_graph(self, test_cases: List[TestCase]):
|
||||
"""构建依赖关系图
|
||||
|
||||
Args:
|
||||
test_cases: 测试用例列表
|
||||
"""
|
||||
self._dependency_map.clear()
|
||||
self._reverse_dependency_map.clear()
|
||||
|
||||
for test_case in test_cases:
|
||||
for dep_id in test_case.dependencies:
|
||||
self._dependency_map[test_case.id].append(dep_id)
|
||||
self._reverse_dependency_map[dep_id].append(test_case.id)
|
||||
|
||||
if self.logger:
|
||||
self.logger.debug(f"依赖关系图构建完成: {len(self._dependency_map)} 个依赖关系")
|
||||
|
||||
def _topological_sort(self, test_cases: List[TestCase]) -> List[TestCase]:
|
||||
"""拓扑排序测试用例
|
||||
|
||||
Args:
|
||||
test_cases: 测试用例列表
|
||||
|
||||
Returns:
|
||||
排序后的测试用例列表
|
||||
|
||||
Raises:
|
||||
TestRunException: 存在循环依赖时抛出
|
||||
"""
|
||||
self._build_dependency_graph(test_cases)
|
||||
|
||||
in_degree = {tc.id: 0 for tc in test_cases}
|
||||
test_case_map = {tc.id: tc for tc in test_cases}
|
||||
|
||||
for tc in test_cases:
|
||||
for dep_id in tc.dependencies:
|
||||
if dep_id in in_degree:
|
||||
in_degree[tc.id] += 1
|
||||
|
||||
queue = [tc_id for tc_id, degree in in_degree.items() if degree == 0]
|
||||
result = []
|
||||
|
||||
while queue:
|
||||
current_id = queue.pop(0)
|
||||
result.append(test_case_map[current_id])
|
||||
|
||||
for dependent_id in self._reverse_dependency_map[current_id]:
|
||||
in_degree[dependent_id] -= 1
|
||||
if in_degree[dependent_id] == 0:
|
||||
queue.append(dependent_id)
|
||||
|
||||
if len(result) != len(test_cases):
|
||||
raise TestRunException("存在循环依赖,无法确定测试用例执行顺序")
|
||||
|
||||
return result
|
||||
|
||||
def _prepare_request_data(self, test_case: TestCase) -> Dict[str, Any]:
|
||||
"""准备请求数据
|
||||
|
||||
Args:
|
||||
test_case: 测试用例
|
||||
|
||||
Returns:
|
||||
准备好的请求数据
|
||||
"""
|
||||
params = test_case.params.copy() if test_case.params else {}
|
||||
body = test_case.body.copy() if test_case.body else {}
|
||||
|
||||
params = self._resolve_context_variables(params)
|
||||
body = self._resolve_context_variables(body)
|
||||
|
||||
return {"params": params, "body": body}
|
||||
|
||||
def _resolve_context_variables(self, data: Any) -> Any:
|
||||
"""解析上下文变量
|
||||
|
||||
Args:
|
||||
data: 数据
|
||||
|
||||
Returns:
|
||||
解析后的数据
|
||||
"""
|
||||
if isinstance(data, str):
|
||||
if data.startswith("${") and data.endswith("}"):
|
||||
var_name = data[2:-1]
|
||||
return self.get_context(var_name, data)
|
||||
return data
|
||||
elif isinstance(data, dict):
|
||||
return {k: self._resolve_context_variables(v) for k, v in data.items()}
|
||||
elif isinstance(data, list):
|
||||
return [self._resolve_context_variables(item) for item in data]
|
||||
else:
|
||||
return data
|
||||
|
||||
def _execute_setup(self, test_case: TestCase):
|
||||
"""执行前置操作
|
||||
|
||||
Args:
|
||||
test_case: 测试用例
|
||||
"""
|
||||
if not test_case.setup:
|
||||
return
|
||||
|
||||
setup_type = test_case.setup.get("type")
|
||||
|
||||
if setup_type == "set_context":
|
||||
key = test_case.setup.get("key")
|
||||
value = test_case.setup.get("value")
|
||||
self.set_context(key, value)
|
||||
elif setup_type == "sleep":
|
||||
import time
|
||||
time.sleep(test_case.setup.get("seconds", 1))
|
||||
|
||||
def _execute_teardown(self, test_case: TestCase):
|
||||
"""执行后置操作
|
||||
|
||||
Args:
|
||||
test_case: 测试用例
|
||||
"""
|
||||
if not test_case.teardown:
|
||||
return
|
||||
|
||||
teardown_type = test_case.teardown.get("type")
|
||||
|
||||
if teardown_type == "clear_context":
|
||||
key = test_case.teardown.get("key")
|
||||
if key in self._context:
|
||||
del self._context[key]
|
||||
elif teardown_type == "sleep":
|
||||
import time
|
||||
time.sleep(test_case.teardown.get("seconds", 1))
|
||||
|
||||
def _execute_test_case(self, test_case: TestCase) -> TestResult:
|
||||
"""执行单个测试用例
|
||||
|
||||
Args:
|
||||
test_case: 测试用例
|
||||
|
||||
Returns:
|
||||
测试结果
|
||||
"""
|
||||
if self.logger:
|
||||
self.logger.info(f"执行测试用例: {test_case.name} ({test_case.id})")
|
||||
|
||||
try:
|
||||
self._execute_setup(test_case)
|
||||
|
||||
request_data = self._prepare_request_data(test_case)
|
||||
|
||||
headers = test_case.headers.copy() if test_case.headers else {}
|
||||
|
||||
if test_case.auth_required and self.auth_manager:
|
||||
auth_headers = self.auth_manager.get_auth_headers()
|
||||
headers.update(auth_headers)
|
||||
|
||||
response_data = self.api_client.request(
|
||||
method=test_case.method,
|
||||
endpoint=test_case.endpoint,
|
||||
headers=headers,
|
||||
params=request_data.get("params"),
|
||||
body=request_data.get("body"),
|
||||
retry_count=test_case.retry_count
|
||||
)
|
||||
|
||||
status_code = response_data["status_code"]
|
||||
response_body = response_data["response_body"]
|
||||
response_headers = response_data["response_headers"]
|
||||
performance = response_data["performance"]
|
||||
|
||||
passed, error_message = self.validation_engine.validate_response(
|
||||
test_case,
|
||||
status_code,
|
||||
response_body,
|
||||
response_headers
|
||||
)
|
||||
|
||||
if passed and test_case.validations:
|
||||
self._extract_response_data(test_case, response_body)
|
||||
|
||||
test_result = TestResult(
|
||||
test_case=test_case,
|
||||
passed=passed,
|
||||
status_code=status_code,
|
||||
response_body=response_body,
|
||||
response_headers=response_headers,
|
||||
error_message=error_message if not passed else None,
|
||||
performance=performance,
|
||||
execution_time=performance.response_time / 1000.0,
|
||||
retry_count=test_case.retry_count
|
||||
)
|
||||
|
||||
self._execute_teardown(test_case)
|
||||
|
||||
if self.logger:
|
||||
if passed:
|
||||
self.logger.info(f"测试用例通过: {test_case.name}")
|
||||
else:
|
||||
self.logger.error(f"测试用例失败: {test_case.name} - {error_message}")
|
||||
|
||||
return test_result
|
||||
|
||||
except RequestException as e:
|
||||
if self.logger:
|
||||
self.logger.error(f"请求异常: {test_case.name} - {str(e)}")
|
||||
|
||||
return TestResult(
|
||||
test_case=test_case,
|
||||
passed=False,
|
||||
status_code=0,
|
||||
response_body=None,
|
||||
response_headers={},
|
||||
error_message=f"请求异常: {str(e)}"
|
||||
)
|
||||
except Exception as e:
|
||||
if self.logger:
|
||||
self.logger.error(f"执行异常: {test_case.name} - {str(e)}")
|
||||
|
||||
return TestResult(
|
||||
test_case=test_case,
|
||||
passed=False,
|
||||
status_code=0,
|
||||
response_body=None,
|
||||
response_headers={},
|
||||
error_message=f"执行异常: {str(e)}"
|
||||
)
|
||||
|
||||
def _extract_response_data(self, test_case: TestCase, response_body: Any):
|
||||
"""提取响应数据到上下文
|
||||
|
||||
Args:
|
||||
test_case: 测试用例
|
||||
response_body: 响应体
|
||||
"""
|
||||
extract_config = test_case.validations or []
|
||||
|
||||
for validation in extract_config:
|
||||
if validation.get("type") == "extract":
|
||||
field = validation.get("field")
|
||||
var_name = validation.get("var_name", field)
|
||||
|
||||
if isinstance(response_body, dict) and field in response_body:
|
||||
self.set_context(var_name, response_body[field])
|
||||
|
||||
def execute_test_suite(
|
||||
self,
|
||||
test_cases: List[TestCase],
|
||||
stop_on_failure: bool = False
|
||||
) -> TestSuiteResult:
|
||||
"""执行测试套件
|
||||
|
||||
Args:
|
||||
test_cases: 测试用例列表
|
||||
stop_on_failure: 是否在失败时停止
|
||||
|
||||
Returns:
|
||||
测试套件结果
|
||||
"""
|
||||
if self.logger:
|
||||
self.logger.info(f"开始执行测试套件,共 {len(test_cases)} 个测试用例")
|
||||
|
||||
self._context.clear()
|
||||
|
||||
sorted_test_cases = self._topological_sort(test_cases)
|
||||
|
||||
results = []
|
||||
|
||||
for test_case in sorted_test_cases:
|
||||
if not test_case.enabled:
|
||||
if self.logger:
|
||||
self.logger.info(f"跳过已禁用的测试用例: {test_case.name}")
|
||||
continue
|
||||
|
||||
result = self._execute_test_case(test_case)
|
||||
results.append(result)
|
||||
|
||||
if not result.passed and stop_on_failure:
|
||||
if self.logger:
|
||||
self.logger.warning(f"测试失败,停止执行: {test_case.name}")
|
||||
break
|
||||
|
||||
passed_count = sum(1 for r in results if r.passed)
|
||||
failed_count = sum(1 for r in results if not r.passed)
|
||||
skipped_count = len(test_cases) - len(results)
|
||||
|
||||
test_suite_result = TestSuiteResult(
|
||||
suite_name="Test Suite",
|
||||
total=len(test_cases),
|
||||
passed=passed_count,
|
||||
failed=failed_count,
|
||||
skipped=skipped_count,
|
||||
results=results,
|
||||
start_time=datetime.now()
|
||||
)
|
||||
|
||||
if self.logger:
|
||||
self.logger.info(
|
||||
f"测试套件执行完成: 通过 {test_suite_result.passed}, "
|
||||
f"失败 {test_suite_result.failed}, "
|
||||
f"跳过 {test_suite_result.skipped}"
|
||||
)
|
||||
|
||||
return test_suite_result
|
||||
|
||||
def execute_test_cases_by_filter(
|
||||
self,
|
||||
test_cases: List[TestCase],
|
||||
module_filter: Optional[str] = None,
|
||||
tag_filter: Optional[List[str]] = None,
|
||||
priority_filter: Optional[int] = None
|
||||
) -> TestSuiteResult:
|
||||
"""按过滤条件执行测试用例
|
||||
|
||||
Args:
|
||||
test_cases: 测试用例列表
|
||||
module_filter: 模块过滤
|
||||
tag_filter: 标签过滤
|
||||
priority_filter: 优先级过滤
|
||||
|
||||
Returns:
|
||||
测试套件结果
|
||||
"""
|
||||
filtered_cases = []
|
||||
|
||||
for test_case in test_cases:
|
||||
if module_filter and test_case.module != module_filter:
|
||||
continue
|
||||
|
||||
if tag_filter and not any(tag in test_case.tags for tag in tag_filter):
|
||||
continue
|
||||
|
||||
if priority_filter is not None and test_case.priority != priority_filter:
|
||||
continue
|
||||
|
||||
filtered_cases.append(test_case)
|
||||
|
||||
if self.logger:
|
||||
self.logger.info(f"过滤后执行 {len(filtered_cases)} 个测试用例")
|
||||
|
||||
return self.execute_test_suite(filtered_cases)
|
||||
@@ -0,0 +1,337 @@
|
||||
from typing import Dict, Any, List
|
||||
import json
|
||||
import re
|
||||
from apitest.models.test_models import TestCase, TestResult, PerformanceMetrics
|
||||
from apitest.models.exceptions import ValidationException
|
||||
|
||||
|
||||
class ValidationEngine:
|
||||
"""验证引擎"""
|
||||
|
||||
def __init__(self, logger=None):
|
||||
"""初始化验证引擎
|
||||
|
||||
Args:
|
||||
logger: 日志记录器
|
||||
"""
|
||||
self.logger = logger
|
||||
|
||||
def validate_response(
|
||||
self,
|
||||
test_case: TestCase,
|
||||
status_code: int,
|
||||
response_body: Any,
|
||||
response_headers: Dict[str, str]
|
||||
) -> tuple[bool, str]:
|
||||
"""验证响应
|
||||
|
||||
Args:
|
||||
test_case: 测试用例
|
||||
status_code: HTTP状态码
|
||||
response_body: 响应体
|
||||
response_headers: 响应头
|
||||
|
||||
Returns:
|
||||
(是否通过, 错误消息)
|
||||
"""
|
||||
if not test_case.validations:
|
||||
return True, ""
|
||||
|
||||
for validation in test_case.validations:
|
||||
passed, error = self._execute_validation(
|
||||
validation,
|
||||
status_code,
|
||||
response_body,
|
||||
response_headers
|
||||
)
|
||||
|
||||
if not passed:
|
||||
return False, error
|
||||
|
||||
return True, ""
|
||||
|
||||
def _execute_validation(
|
||||
self,
|
||||
validation: Dict[str, Any],
|
||||
status_code: int,
|
||||
response_body: Any,
|
||||
response_headers: Dict[str, str]
|
||||
) -> tuple[bool, str]:
|
||||
"""执行单个验证规则
|
||||
|
||||
Args:
|
||||
validation: 验证规则
|
||||
status_code: HTTP状态码
|
||||
response_body: 响应体
|
||||
response_headers: 响应头
|
||||
|
||||
Returns:
|
||||
(是否通过, 错误消息)
|
||||
"""
|
||||
validation_type = validation.get("type")
|
||||
|
||||
if validation_type == "status_code":
|
||||
return self._validate_status_code(validation, status_code)
|
||||
elif validation_type == "contains":
|
||||
return self._validate_contains(validation, response_body)
|
||||
elif validation_type == "equals":
|
||||
return self._validate_equals(validation, response_body)
|
||||
elif validation_type == "json_path":
|
||||
return self._validate_json_path(validation, response_body)
|
||||
elif validation_type == "regex":
|
||||
return self._validate_regex(validation, response_body)
|
||||
elif validation_type == "header":
|
||||
return self._validate_header(validation, response_headers)
|
||||
elif validation_type == "response_time":
|
||||
return self._validate_response_time(validation)
|
||||
elif validation_type == "schema":
|
||||
return self._validate_schema(validation, response_body)
|
||||
else:
|
||||
return False, f"不支持的验证类型: {validation_type}"
|
||||
|
||||
def _validate_status_code(self, validation: Dict[str, Any], status_code: int) -> tuple[bool, str]:
|
||||
"""验证状态码
|
||||
|
||||
Args:
|
||||
validation: 验证规则
|
||||
status_code: HTTP状态码
|
||||
|
||||
Returns:
|
||||
(是否通过, 错误消息)
|
||||
"""
|
||||
expected_code = validation.get("value")
|
||||
if status_code == expected_code:
|
||||
return True, ""
|
||||
|
||||
return False, f"状态码验证失败: 期望 {expected_code}, 实际 {status_code}"
|
||||
|
||||
def _validate_contains(self, validation: Dict[str, Any], response_body: Any) -> tuple[bool, str]:
|
||||
"""验证响应体包含指定内容
|
||||
|
||||
Args:
|
||||
validation: 验证规则
|
||||
response_body: 响应体
|
||||
|
||||
Returns:
|
||||
(是否通过, 错误消息)
|
||||
"""
|
||||
expected_value = validation.get("value")
|
||||
field = validation.get("field")
|
||||
|
||||
if field:
|
||||
if isinstance(response_body, dict):
|
||||
actual_value = response_body.get(field)
|
||||
else:
|
||||
return False, f"响应体不是字典类型,无法访问字段: {field}"
|
||||
else:
|
||||
actual_value = response_body
|
||||
|
||||
if str(expected_value) in str(actual_value):
|
||||
return True, ""
|
||||
|
||||
return False, f"包含验证失败: 响应体中未找到 '{expected_value}'"
|
||||
|
||||
def _validate_equals(self, validation: Dict[str, Any], response_body: Any) -> tuple[bool, str]:
|
||||
"""验证响应体等于指定值
|
||||
|
||||
Args:
|
||||
validation: 验证规则
|
||||
response_body: 响应体
|
||||
|
||||
Returns:
|
||||
(是否通过, 错误消息)
|
||||
"""
|
||||
expected_value = validation.get("value")
|
||||
field = validation.get("field")
|
||||
|
||||
if field:
|
||||
if isinstance(response_body, dict):
|
||||
actual_value = response_body.get(field)
|
||||
else:
|
||||
return False, f"响应体不是字典类型,无法访问字段: {field}"
|
||||
else:
|
||||
actual_value = response_body
|
||||
|
||||
if actual_value == expected_value:
|
||||
return True, ""
|
||||
|
||||
return False, f"相等验证失败: 期望 {expected_value}, 实际 {actual_value}"
|
||||
|
||||
def _validate_json_path(self, validation: Dict[str, Any], response_body: Any) -> tuple[bool, str]:
|
||||
"""验证JSON路径
|
||||
|
||||
Args:
|
||||
validation: 验证规则
|
||||
response_body: 响应体
|
||||
|
||||
Returns:
|
||||
(是否通过, 错误消息)
|
||||
"""
|
||||
path = validation.get("path")
|
||||
expected_value = validation.get("value")
|
||||
|
||||
try:
|
||||
actual_value = self._get_json_path_value(response_body, path)
|
||||
|
||||
if actual_value == expected_value:
|
||||
return True, ""
|
||||
|
||||
return False, f"JSON路径验证失败: {path} 期望 {expected_value}, 实际 {actual_value}"
|
||||
|
||||
except (KeyError, IndexError, TypeError) as e:
|
||||
return False, f"JSON路径访问失败: {path} - {str(e)}"
|
||||
|
||||
def _get_json_path_value(self, data: Any, path: str) -> Any:
|
||||
"""获取JSON路径值
|
||||
|
||||
Args:
|
||||
data: 数据
|
||||
path: JSON路径
|
||||
|
||||
Returns:
|
||||
路径对应的值
|
||||
"""
|
||||
parts = path.split(".")
|
||||
current = data
|
||||
|
||||
for part in parts:
|
||||
if isinstance(current, dict):
|
||||
current = current[part]
|
||||
elif isinstance(current, list) and part.isdigit():
|
||||
current = current[int(part)]
|
||||
else:
|
||||
raise KeyError(f"无法访问路径: {part}")
|
||||
|
||||
return current
|
||||
|
||||
def _validate_regex(self, validation: Dict[str, Any], response_body: Any) -> tuple[bool, str]:
|
||||
"""验证正则表达式
|
||||
|
||||
Args:
|
||||
validation: 验证规则
|
||||
response_body: 响应体
|
||||
|
||||
Returns:
|
||||
(是否通过, 错误消息)
|
||||
"""
|
||||
pattern = validation.get("pattern")
|
||||
field = validation.get("field")
|
||||
|
||||
if field:
|
||||
if isinstance(response_body, dict):
|
||||
actual_value = str(response_body.get(field, ""))
|
||||
else:
|
||||
actual_value = str(response_body)
|
||||
else:
|
||||
actual_value = str(response_body)
|
||||
|
||||
if re.search(pattern, actual_value):
|
||||
return True, ""
|
||||
|
||||
return False, f"正则表达式验证失败: '{actual_value}' 不匹配模式 '{pattern}'"
|
||||
|
||||
def _validate_header(self, validation: Dict[str, Any], response_headers: Dict[str, str]) -> tuple[bool, str]:
|
||||
"""验证响应头
|
||||
|
||||
Args:
|
||||
validation: 验证规则
|
||||
response_headers: 响应头
|
||||
|
||||
Returns:
|
||||
(是否通过, 错误消息)
|
||||
"""
|
||||
header_name = validation.get("name")
|
||||
expected_value = validation.get("value")
|
||||
|
||||
actual_value = response_headers.get(header_name)
|
||||
|
||||
if actual_value is None:
|
||||
return False, f"响应头中未找到: {header_name}"
|
||||
|
||||
if expected_value and actual_value != expected_value:
|
||||
return False, f"响应头验证失败: {header_name} 期望 {expected_value}, 实际 {actual_value}"
|
||||
|
||||
return True, ""
|
||||
|
||||
def _validate_response_time(self, validation: Dict[str, Any]) -> tuple[bool, str]:
|
||||
"""验证响应时间(需要在TestResult中检查)
|
||||
|
||||
Args:
|
||||
validation: 验证规则
|
||||
|
||||
Returns:
|
||||
(是否通过, 错误消息)
|
||||
"""
|
||||
max_time = validation.get("max_time")
|
||||
|
||||
return True, ""
|
||||
|
||||
def _validate_schema(self, validation: Dict[str, Any], response_body: Any) -> tuple[bool, str]:
|
||||
"""验证响应体结构
|
||||
|
||||
Args:
|
||||
validation: 验证规则
|
||||
response_body: 响应体
|
||||
|
||||
Returns:
|
||||
(是否通过, 错误消息)
|
||||
"""
|
||||
schema = validation.get("schema")
|
||||
|
||||
if not isinstance(response_body, dict):
|
||||
return False, f"响应体不是字典类型,无法验证结构"
|
||||
|
||||
for field, field_type in schema.items():
|
||||
if field not in response_body:
|
||||
return False, f"响应体中缺少字段: {field}"
|
||||
|
||||
actual_type = type(response_body[field]).__name__
|
||||
expected_type = field_type
|
||||
|
||||
if actual_type != expected_type:
|
||||
return False, f"字段 {field} 类型错误: 期望 {expected_type}, 实际 {actual_type}"
|
||||
|
||||
return True, ""
|
||||
|
||||
def validate_performance(
|
||||
self,
|
||||
performance: PerformanceMetrics,
|
||||
max_response_time: int
|
||||
) -> tuple[bool, str]:
|
||||
"""验证性能指标
|
||||
|
||||
Args:
|
||||
performance: 性能指标
|
||||
max_response_time: 最大响应时间(毫秒)
|
||||
|
||||
Returns:
|
||||
(是否通过, 错误消息)
|
||||
"""
|
||||
if performance.response_time > max_response_time:
|
||||
return False, f"响应时间超过阈值: {performance.response_time}ms > {max_response_time}ms"
|
||||
|
||||
return True, ""
|
||||
|
||||
def validate_test_result(
|
||||
self,
|
||||
test_result: TestResult,
|
||||
max_response_time: int
|
||||
) -> tuple[bool, str]:
|
||||
"""验证测试结果
|
||||
|
||||
Args:
|
||||
test_result: 测试结果
|
||||
max_response_time: 最大响应时间(毫秒)
|
||||
|
||||
Returns:
|
||||
(是否通过, 错误消息)
|
||||
"""
|
||||
if not test_result.passed:
|
||||
return False, test_result.error_message or "测试失败"
|
||||
|
||||
if test_result.performance:
|
||||
passed, error = self.validate_performance(test_result.performance, max_response_time)
|
||||
if not passed:
|
||||
return False, error
|
||||
|
||||
return True, ""
|
||||
@@ -0,0 +1,267 @@
|
||||
"""测试数据管理模块"""
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
from pathlib import Path
|
||||
import json
|
||||
import csv
|
||||
from apitest.models.test_models import TestCase, HTTPMethod
|
||||
from apitest.models.exceptions import TestRunException
|
||||
|
||||
|
||||
class TestDataManager:
|
||||
"""测试数据管理器"""
|
||||
|
||||
def __init__(self, logger=None):
|
||||
"""初始化测试数据管理器
|
||||
|
||||
Args:
|
||||
logger: 日志记录器
|
||||
"""
|
||||
self.logger = logger
|
||||
|
||||
def load_test_cases_from_json(self, file_path: Path) -> List[TestCase]:
|
||||
"""从JSON文件加载测试用例
|
||||
|
||||
Args:
|
||||
file_path: JSON文件路径
|
||||
|
||||
Returns:
|
||||
测试用例列表
|
||||
|
||||
Raises:
|
||||
TestRunException: 加载失败
|
||||
"""
|
||||
try:
|
||||
if not file_path.exists():
|
||||
raise TestRunException(f"测试用例文件不存在: {file_path}")
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
test_cases = []
|
||||
for item in data:
|
||||
method_str = item.get("method", "GET")
|
||||
try:
|
||||
method = HTTPMethod(method_str)
|
||||
except ValueError:
|
||||
method = HTTPMethod.GET
|
||||
|
||||
test_case = TestCase(
|
||||
id=item.get("id", ""),
|
||||
name=item.get("name", ""),
|
||||
description=item.get("description", ""),
|
||||
module=item.get("module", ""),
|
||||
endpoint=item.get("endpoint", ""),
|
||||
method=method,
|
||||
headers=item.get("headers", {}),
|
||||
params=item.get("params"),
|
||||
body=item.get("body"),
|
||||
dependencies=item.get("dependencies", []),
|
||||
tags=item.get("tags", []),
|
||||
priority=item.get("priority", 0),
|
||||
enabled=item.get("enabled", True),
|
||||
timeout=item.get("timeout"),
|
||||
validations=item.get("validations", [])
|
||||
)
|
||||
test_cases.append(test_case)
|
||||
|
||||
if self.logger:
|
||||
self.logger.info(f"从JSON文件成功加载 {len(test_cases)} 个测试用例: {file_path}")
|
||||
|
||||
return test_cases
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
error_msg = f"JSON文件解析失败: {str(e)}"
|
||||
if self.logger:
|
||||
self.logger.error(error_msg)
|
||||
raise TestRunException(error_msg) from e
|
||||
except Exception as e:
|
||||
error_msg = f"加载测试用例失败: {str(e)}"
|
||||
if self.logger:
|
||||
self.logger.error(error_msg)
|
||||
raise TestRunException(error_msg) from e
|
||||
|
||||
def load_test_data_from_csv(self, file_path: Path) -> List[Dict[str, Any]]:
|
||||
"""从CSV文件加载测试数据
|
||||
|
||||
Args:
|
||||
file_path: CSV文件路径
|
||||
|
||||
Returns:
|
||||
测试数据列表
|
||||
|
||||
Raises:
|
||||
TestRunException: 加载失败
|
||||
"""
|
||||
try:
|
||||
if not file_path.exists():
|
||||
raise TestRunException(f"测试数据文件不存在: {file_path}")
|
||||
|
||||
test_data = []
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
test_data.append(dict(row))
|
||||
|
||||
if self.logger:
|
||||
self.logger.info(f"从CSV文件成功加载 {len(test_data)} 条测试数据: {file_path}")
|
||||
|
||||
return test_data
|
||||
|
||||
except csv.Error as e:
|
||||
error_msg = f"CSV文件解析失败: {str(e)}"
|
||||
if self.logger:
|
||||
self.logger.error(error_msg)
|
||||
raise TestRunException(error_msg) from e
|
||||
except Exception as e:
|
||||
error_msg = f"加载测试数据失败: {str(e)}"
|
||||
if self.logger:
|
||||
self.logger.error(error_msg)
|
||||
raise TestRunException(error_msg) from e
|
||||
|
||||
def parameterize_test_case(
|
||||
self,
|
||||
test_case: TestCase,
|
||||
test_data: List[Dict[str, Any]]
|
||||
) -> List[TestCase]:
|
||||
"""使用测试数据参数化测试用例
|
||||
|
||||
Args:
|
||||
test_case: 原始测试用例
|
||||
test_data: 测试数据列表
|
||||
|
||||
Returns:
|
||||
参数化后的测试用例列表
|
||||
"""
|
||||
try:
|
||||
parameterized_cases = []
|
||||
|
||||
for i, data in enumerate(test_data):
|
||||
new_id = f"{test_case.id}_{i+1}"
|
||||
new_name = f"{test_case.name} (数据集 {i+1})"
|
||||
|
||||
params = test_case.params.copy() if test_case.params else {}
|
||||
body = test_case.body.copy() if test_case.body else {}
|
||||
|
||||
params.update(data.get("params", {}))
|
||||
body.update(data.get("body", {}))
|
||||
|
||||
parameterized_case = TestCase(
|
||||
id=new_id,
|
||||
name=new_name,
|
||||
description=test_case.description,
|
||||
module=test_case.module,
|
||||
endpoint=test_case.endpoint,
|
||||
method=test_case.method,
|
||||
headers=test_case.headers,
|
||||
params=params,
|
||||
body=body,
|
||||
dependencies=test_case.dependencies,
|
||||
tags=test_case.tags,
|
||||
priority=test_case.priority,
|
||||
enabled=test_case.enabled,
|
||||
timeout=test_case.timeout,
|
||||
validations=test_case.validations
|
||||
)
|
||||
|
||||
parameterized_cases.append(parameterized_case)
|
||||
|
||||
if self.logger:
|
||||
self.logger.info(f"使用 {len(test_data)} 条测试数据参数化测试用例: {test_case.id}")
|
||||
|
||||
return parameterized_cases
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"参数化测试用例失败: {str(e)}"
|
||||
if self.logger:
|
||||
self.logger.error(error_msg)
|
||||
raise TestRunException(error_msg) from e
|
||||
|
||||
def save_test_cases_to_json(
|
||||
self,
|
||||
test_cases: List[TestCase],
|
||||
file_path: Path
|
||||
) -> None:
|
||||
"""将测试用例保存到JSON文件
|
||||
|
||||
Args:
|
||||
test_cases: 测试用例列表
|
||||
file_path: 输出文件路径
|
||||
|
||||
Raises:
|
||||
TestRunException: 保存失败
|
||||
"""
|
||||
try:
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
data = []
|
||||
for test_case in test_cases:
|
||||
item = {
|
||||
"id": test_case.id,
|
||||
"name": test_case.name,
|
||||
"description": test_case.description,
|
||||
"module": test_case.module,
|
||||
"endpoint": test_case.endpoint,
|
||||
"method": test_case.method.value,
|
||||
"headers": test_case.headers,
|
||||
"params": test_case.params,
|
||||
"body": test_case.body,
|
||||
"dependencies": test_case.dependencies,
|
||||
"tags": test_case.tags,
|
||||
"priority": test_case.priority,
|
||||
"enabled": test_case.enabled,
|
||||
"timeout": test_case.timeout,
|
||||
"validations": test_case.validations
|
||||
}
|
||||
data.append(item)
|
||||
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
if self.logger:
|
||||
self.logger.info(f"成功保存 {len(test_cases)} 个测试用例到JSON文件: {file_path}")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"保存测试用例失败: {str(e)}"
|
||||
if self.logger:
|
||||
self.logger.error(error_msg)
|
||||
raise TestRunException(error_msg) from e
|
||||
|
||||
def save_test_data_to_csv(
|
||||
self,
|
||||
test_data: List[Dict[str, Any]],
|
||||
file_path: Path,
|
||||
fieldnames: Optional[List[str]] = None
|
||||
) -> None:
|
||||
"""将测试数据保存到CSV文件
|
||||
|
||||
Args:
|
||||
test_data: 测试数据列表
|
||||
file_path: 输出文件路径
|
||||
fieldnames: 字段名列表,如果为None则自动推断
|
||||
|
||||
Raises:
|
||||
TestRunException: 保存失败
|
||||
"""
|
||||
try:
|
||||
if not test_data:
|
||||
raise TestRunException("测试数据为空")
|
||||
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if fieldnames is None:
|
||||
fieldnames = list(test_data[0].keys())
|
||||
|
||||
with open(file_path, "w", encoding="utf-8", newline="") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction='ignore')
|
||||
writer.writeheader()
|
||||
writer.writerows(test_data)
|
||||
|
||||
if self.logger:
|
||||
self.logger.info(f"成功保存 {len(test_data)} 条测试数据到CSV文件: {file_path}")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"保存测试数据失败: {str(e)}"
|
||||
if self.logger:
|
||||
self.logger.error(error_msg)
|
||||
raise TestRunException(error_msg) from e
|
||||
@@ -0,0 +1,163 @@
|
||||
import click
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from apitest.config.config_manager import ConfigManager
|
||||
from apitest.core.test_orchestrator import TestOrchestrator
|
||||
from apitest.report.report_manager import ReportManager
|
||||
from apitest.utils.logger_manager import LoggerManager
|
||||
|
||||
|
||||
def setup_logger(config_manager: ConfigManager) -> LoggerManager:
|
||||
logger_manager = LoggerManager(config_manager)
|
||||
logger_manager.setup()
|
||||
return logger_manager
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version="1.0.0")
|
||||
def cli():
|
||||
"""黑盒API测试工具"""
|
||||
pass
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--suite", default="all", help="测试套件名称")
|
||||
@click.option("--filter", help="测试用例过滤器(如:priority=high,module=user)")
|
||||
@click.option("--parallel", is_flag=True, help="并发执行测试")
|
||||
@click.option("--threads", default=4, help="并发线程数")
|
||||
@click.option("--verbose", is_flag=True, help="详细输出")
|
||||
def run(suite: str, filter: Optional[str], parallel: bool, threads: int, verbose: bool):
|
||||
"""运行测试用例"""
|
||||
try:
|
||||
config_manager = ConfigManager()
|
||||
logger_manager = setup_logger(config_manager)
|
||||
logger = logger_manager.get_logger(__name__)
|
||||
|
||||
logger.info(f"开始执行测试套件: {suite}")
|
||||
if filter:
|
||||
logger.info(f"过滤器: {filter}")
|
||||
if parallel:
|
||||
logger.info(f"并发模式: {threads} 线程")
|
||||
|
||||
orchestrator = TestOrchestrator(config_manager, logger_manager)
|
||||
|
||||
filters = {}
|
||||
if filter:
|
||||
for f in filter.split(","):
|
||||
key, value = f.split("=")
|
||||
filters[key] = value
|
||||
|
||||
results = orchestrator.run_suite(suite, filters, parallel, threads)
|
||||
|
||||
report_manager = ReportManager(config_manager, logger_manager)
|
||||
report_manager.generate_report(results)
|
||||
|
||||
logger.info(f"测试完成: 通过 {results.passed}, 失败 {results.failed}, 跳过 {results.skipped}")
|
||||
|
||||
sys.exit(0 if results.failed == 0 else 1)
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"执行测试时出错: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--suite", default="all", help="测试套件名称")
|
||||
@click.option("--filter", help="测试用例过滤器")
|
||||
def list(suite: str, filter: Optional[str]):
|
||||
"""列出测试用例"""
|
||||
try:
|
||||
config_manager = ConfigManager()
|
||||
logger_manager = setup_logger(config_manager)
|
||||
|
||||
orchestrator = TestOrchestrator(config_manager, logger_manager)
|
||||
|
||||
filters = {}
|
||||
if filter:
|
||||
for f in filter.split(","):
|
||||
key, value = f.split("=")
|
||||
filters[key] = value
|
||||
|
||||
test_cases = orchestrator.list_test_cases(suite, filters)
|
||||
|
||||
click.echo(f"\n测试套件: {suite}")
|
||||
click.echo(f"测试用例数量: {len(test_cases)}\n")
|
||||
|
||||
for test_case in test_cases:
|
||||
status = "✓" if test_case.enabled else "✗"
|
||||
click.echo(f"{status} {test_case.id}: {test_case.name}")
|
||||
click.echo(f" 模块: {test_case.module}")
|
||||
click.echo(f" 方法: {test_case.method.value} {test_case.endpoint}")
|
||||
click.echo(f" 优先级: {test_case.priority}")
|
||||
click.echo()
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"列出测试用例时出错: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--format", default="html", help="报告格式(html/json)")
|
||||
@click.option("--output", help="输出文件路径")
|
||||
@click.option("--suite", default="all", help="测试套件名称")
|
||||
def report(format: str, output: Optional[str], suite: str):
|
||||
"""生成测试报告"""
|
||||
try:
|
||||
config_manager = ConfigManager()
|
||||
logger_manager = setup_logger(config_manager)
|
||||
|
||||
report_manager = ReportManager(config_manager, logger_manager)
|
||||
|
||||
if output:
|
||||
report_manager.generate_report_from_history(suite, format, output)
|
||||
else:
|
||||
report_manager.generate_latest_report(format)
|
||||
|
||||
click.echo(f"报告已生成: {format}")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"生成报告时出错: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--set", help="设置配置值(格式:key=value)")
|
||||
@click.option("--get", help="获取配置值")
|
||||
@click.option("--validate", is_flag=True, help="验证配置")
|
||||
def config(set: Optional[str], get: Optional[str], validate: bool):
|
||||
"""配置管理"""
|
||||
try:
|
||||
config_manager = ConfigManager()
|
||||
|
||||
if set:
|
||||
key, value = set.split("=")
|
||||
config_manager.set(key, value)
|
||||
click.echo(f"配置已设置: {key} = {value}")
|
||||
|
||||
elif get:
|
||||
value = config_manager.get(get)
|
||||
click.echo(f"{get} = {value}")
|
||||
|
||||
elif validate:
|
||||
is_valid, errors = config_manager.validate()
|
||||
if is_valid:
|
||||
click.echo("配置验证通过 ✓")
|
||||
else:
|
||||
click.echo("配置验证失败 ✗")
|
||||
for error in errors:
|
||||
click.echo(f" - {error}")
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
click.echo("当前配置:")
|
||||
config_manager.print_config()
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"配置管理时出错: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
@@ -0,0 +1,38 @@
|
||||
class APITestException(Exception):
|
||||
"""API测试基础异常"""
|
||||
pass
|
||||
|
||||
|
||||
class ConfigException(APITestException):
|
||||
"""配置异常"""
|
||||
pass
|
||||
|
||||
|
||||
class DataException(APITestException):
|
||||
"""数据异常"""
|
||||
pass
|
||||
|
||||
|
||||
class AuthException(APITestException):
|
||||
"""认证异常"""
|
||||
pass
|
||||
|
||||
|
||||
class RequestException(APITestException):
|
||||
"""请求异常"""
|
||||
pass
|
||||
|
||||
|
||||
class ValidationException(APITestException):
|
||||
"""验证异常"""
|
||||
pass
|
||||
|
||||
|
||||
class TestRunException(APITestException):
|
||||
"""测试执行异常"""
|
||||
pass
|
||||
|
||||
|
||||
class ReportException(APITestException):
|
||||
"""报告生成异常"""
|
||||
pass
|
||||
@@ -0,0 +1,151 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class HTTPMethod(Enum):
|
||||
"""HTTP方法枚举"""
|
||||
GET = "GET"
|
||||
POST = "POST"
|
||||
PUT = "PUT"
|
||||
DELETE = "DELETE"
|
||||
PATCH = "PATCH"
|
||||
HEAD = "HEAD"
|
||||
OPTIONS = "OPTIONS"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ValidationRule:
|
||||
"""验证规则数据模型"""
|
||||
type: str # status_code, json_path, contains, regex, schema
|
||||
expected: Any
|
||||
json_path: Optional[str] = None
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestCase:
|
||||
"""测试用例数据模型"""
|
||||
id: str # 用例唯一标识
|
||||
name: str # 用例名称
|
||||
description: str # 用例描述
|
||||
module: str # 所属模块
|
||||
endpoint: str # API端点
|
||||
method: HTTPMethod # HTTP方法
|
||||
headers: Dict[str, str] # 请求头
|
||||
params: Optional[Dict[str, Any]] = None # URL参数
|
||||
body: Optional[Dict[str, Any]] = None # 请求体
|
||||
auth_required: bool = True # 是否需要认证
|
||||
dependencies: List[str] = None # 依赖的用例ID
|
||||
timeout: int = 5000 # 超时时间(毫秒)
|
||||
retry_count: int = 0 # 重试次数
|
||||
validations: List[Dict] = None # 验证规则
|
||||
setup: Optional[Dict] = None # 前置操作
|
||||
teardown: Optional[Dict] = None # 后置操作
|
||||
tags: List[str] = None # 标签
|
||||
priority: int = 0 # 优先级
|
||||
enabled: bool = True # 是否启用
|
||||
|
||||
def __post_init__(self):
|
||||
if self.dependencies is None:
|
||||
object.__setattr__(self, "dependencies", [])
|
||||
if self.validations is None:
|
||||
object.__setattr__(self, "validations", [])
|
||||
if self.tags is None:
|
||||
object.__setattr__(self, "tags", [])
|
||||
|
||||
|
||||
@dataclass
|
||||
class PerformanceMetrics:
|
||||
"""性能指标数据模型"""
|
||||
response_time: int # 响应时间(毫秒)
|
||||
request_size: int # 请求大小(字节)
|
||||
response_size: int # 响应大小(字节)
|
||||
timestamp: datetime # 时间戳
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"response_time": self.response_time,
|
||||
"request_size": self.request_size,
|
||||
"response_size": self.response_size,
|
||||
"timestamp": self.timestamp.isoformat()
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestResult:
|
||||
"""测试结果数据模型"""
|
||||
test_case: TestCase # 测试用例
|
||||
passed: bool # 是否通过
|
||||
status_code: int # HTTP状态码
|
||||
response_body: Any # 响应体
|
||||
response_headers: Dict[str, str] # 响应头
|
||||
error_message: Optional[str] = None # 错误消息
|
||||
performance: Optional[PerformanceMetrics] = None # 性能指标
|
||||
execution_time: float = 0.0 # 执行时间(秒)
|
||||
retry_count: int = 0 # 重试次数
|
||||
timestamp: datetime = None # 执行时间戳
|
||||
|
||||
def __post_init__(self):
|
||||
if self.timestamp is None:
|
||||
self.timestamp = datetime.now()
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"test_case_id": self.test_case.id,
|
||||
"test_case_name": self.test_case.name,
|
||||
"passed": self.passed,
|
||||
"status_code": self.status_code,
|
||||
"response_body": self.response_body,
|
||||
"response_headers": self.response_headers,
|
||||
"error_message": self.error_message,
|
||||
"performance": self.performance.to_dict() if self.performance else None,
|
||||
"execution_time": self.execution_time,
|
||||
"retry_count": self.retry_count,
|
||||
"timestamp": self.timestamp.isoformat()
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestSuiteResult:
|
||||
"""测试套件结果数据模型"""
|
||||
suite_name: str # 套件名称
|
||||
total: int # 总数
|
||||
passed: int # 通过数
|
||||
failed: int # 失败数
|
||||
skipped: int # 跳过数
|
||||
results: List[TestResult] # 测试结果列表
|
||||
start_time: datetime # 开始时间
|
||||
end_time: Optional[datetime] = None # 结束时间
|
||||
|
||||
@property
|
||||
def duration(self) -> float:
|
||||
"""执行时长(秒)"""
|
||||
if self.end_time:
|
||||
return (self.end_time - self.start_time).total_seconds()
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def pass_rate(self) -> float:
|
||||
"""通过率"""
|
||||
if self.total == 0:
|
||||
return 0.0
|
||||
return (self.passed / self.total) * 100
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"suite_name": self.suite_name,
|
||||
"total": self.total,
|
||||
"passed": self.passed,
|
||||
"failed": self.failed,
|
||||
"skipped": self.skipped,
|
||||
"pass_rate": self.pass_rate,
|
||||
"duration": self.duration,
|
||||
"start_time": self.start_time.isoformat(),
|
||||
"end_time": self.end_time.isoformat() if self.end_time else None,
|
||||
"results": [result.to_dict() for result in self.results]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
"""测试编排器模块"""
|
||||
|
||||
from apitest.orchestrator.test_orchestrator import TestOrchestrator
|
||||
|
||||
__all__ = ["TestOrchestrator"]
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
"""测试编排器模块"""
|
||||
|
||||
from typing import List, Optional, Dict, Any
|
||||
from pathlib import Path
|
||||
from apitest.models.test_models import TestCase, TestSuiteResult, HTTPMethod
|
||||
from apitest.client.api_client import APIClient
|
||||
from apitest.client.auth_manager import AuthManager
|
||||
from apitest.core.test_engine import TestEngine
|
||||
from apitest.core.validation_engine import ValidationEngine
|
||||
from apitest.report.report_manager import ReportManager
|
||||
from apitest.config.config_manager import ConfigManager
|
||||
from apitest.config.logger_manager import LoggerManager
|
||||
from apitest.models.exceptions import TestRunException
|
||||
|
||||
|
||||
class TestOrchestrator:
|
||||
"""测试编排器"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_manager: Optional[ConfigManager] = None,
|
||||
logger_manager: Optional[LoggerManager] = None,
|
||||
logger=None
|
||||
):
|
||||
"""初始化测试编排器
|
||||
|
||||
Args:
|
||||
config_manager: 配置管理器
|
||||
logger_manager: 日志管理器
|
||||
logger: 日志记录器
|
||||
"""
|
||||
self.config_manager = config_manager or ConfigManager()
|
||||
self.logger_manager = logger_manager or LoggerManager(self.config_manager)
|
||||
self.logger = logger or self.logger_manager.get_logger(__name__)
|
||||
|
||||
self.api_client = APIClient(
|
||||
base_url=self.config_manager.get_base_url(),
|
||||
timeout=self.config_manager.get_timeout(),
|
||||
logger=self.logger
|
||||
)
|
||||
|
||||
self.auth_manager = AuthManager(
|
||||
base_url=self.config_manager.get_base_url(),
|
||||
credentials={},
|
||||
logger=self.logger
|
||||
)
|
||||
|
||||
self.validation_engine = ValidationEngine(logger=self.logger)
|
||||
|
||||
self.test_engine = TestEngine(
|
||||
api_client=self.api_client,
|
||||
auth_manager=self.auth_manager,
|
||||
validation_engine=self.validation_engine,
|
||||
logger=self.logger
|
||||
)
|
||||
|
||||
self.report_manager = ReportManager(logger=self.logger)
|
||||
|
||||
def load_test_cases(self, file_path: Path) -> List[TestCase]:
|
||||
"""加载测试用例
|
||||
|
||||
Args:
|
||||
file_path: 测试用例文件路径
|
||||
|
||||
Returns:
|
||||
测试用例列表
|
||||
|
||||
Raises:
|
||||
TestRunException: 加载失败
|
||||
"""
|
||||
try:
|
||||
import json
|
||||
|
||||
if not file_path.exists():
|
||||
raise TestRunException(f"测试用例文件不存在: {file_path}")
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
test_cases = []
|
||||
for item in data:
|
||||
method_str = item.get("method", "GET")
|
||||
try:
|
||||
method = HTTPMethod(method_str)
|
||||
except ValueError:
|
||||
method = HTTPMethod.GET
|
||||
|
||||
test_case = TestCase(
|
||||
id=item.get("id", ""),
|
||||
name=item.get("name", ""),
|
||||
description=item.get("description", ""),
|
||||
module=item.get("module", ""),
|
||||
endpoint=item.get("endpoint", ""),
|
||||
method=method,
|
||||
headers=item.get("headers", {}),
|
||||
params=item.get("params"),
|
||||
body=item.get("body"),
|
||||
dependencies=item.get("dependencies", []),
|
||||
tags=item.get("tags", []),
|
||||
priority=item.get("priority", 0),
|
||||
enabled=item.get("enabled", True),
|
||||
timeout=item.get("timeout"),
|
||||
validations=item.get("validations", [])
|
||||
)
|
||||
test_cases.append(test_case)
|
||||
|
||||
if self.logger:
|
||||
self.logger.info(f"成功加载 {len(test_cases)} 个测试用例")
|
||||
|
||||
return test_cases
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"加载测试用例失败: {str(e)}"
|
||||
if self.logger:
|
||||
self.logger.error(error_msg)
|
||||
raise TestRunException(error_msg) from e
|
||||
|
||||
def run_test_suite(
|
||||
self,
|
||||
test_cases: List[TestCase],
|
||||
stop_on_failure: bool = False,
|
||||
generate_report: bool = True,
|
||||
report_format: str = "html",
|
||||
report_path: Optional[Path] = None
|
||||
) -> TestSuiteResult:
|
||||
"""运行测试套件
|
||||
|
||||
Args:
|
||||
test_cases: 测试用例列表
|
||||
stop_on_failure: 是否在失败时停止
|
||||
generate_report: 是否生成报告
|
||||
report_format: 报告格式 (html/json)
|
||||
report_path: 报告输出路径
|
||||
|
||||
Returns:
|
||||
测试套件结果
|
||||
"""
|
||||
try:
|
||||
if self.logger:
|
||||
self.logger.info("=" * 50)
|
||||
self.logger.info("开始执行测试套件")
|
||||
self.logger.info("=" * 50)
|
||||
|
||||
result = self.test_engine.execute_test_suite(
|
||||
test_cases,
|
||||
stop_on_failure=stop_on_failure
|
||||
)
|
||||
|
||||
if generate_report:
|
||||
self._generate_report(result, report_format, report_path)
|
||||
|
||||
if self.logger:
|
||||
self.logger.info("=" * 50)
|
||||
self.logger.info("测试套件执行完成")
|
||||
self.logger.info(f"总计: {result.total}, 通过: {result.passed}, 失败: {result.failed}, 跳过: {result.skipped}")
|
||||
self.logger.info(f"通过率: {result.pass_rate:.2f}%")
|
||||
self.logger.info(f"执行时长: {result.duration:.2f}秒")
|
||||
self.logger.info("=" * 50)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"运行测试套件失败: {str(e)}"
|
||||
if self.logger:
|
||||
self.logger.error(error_msg)
|
||||
raise TestRunException(error_msg) from e
|
||||
|
||||
def run_test_suite_by_filter(
|
||||
self,
|
||||
test_cases: List[TestCase],
|
||||
module_filter: Optional[str] = None,
|
||||
tag_filter: Optional[List[str]] = None,
|
||||
priority_filter: Optional[int] = None,
|
||||
stop_on_failure: bool = False,
|
||||
generate_report: bool = True,
|
||||
report_format: str = "html",
|
||||
report_path: Optional[Path] = None
|
||||
) -> TestSuiteResult:
|
||||
"""按过滤条件运行测试套件
|
||||
|
||||
Args:
|
||||
test_cases: 测试用例列表
|
||||
module_filter: 模块过滤
|
||||
tag_filter: 标签过滤
|
||||
priority_filter: 优先级过滤
|
||||
stop_on_failure: 是否在失败时停止
|
||||
generate_report: 是否生成报告
|
||||
report_format: 报告格式 (html/json)
|
||||
report_path: 报告输出路径
|
||||
|
||||
Returns:
|
||||
测试套件结果
|
||||
"""
|
||||
try:
|
||||
if self.logger:
|
||||
self.logger.info("按过滤条件执行测试用例")
|
||||
if module_filter:
|
||||
self.logger.info(f" 模块过滤: {module_filter}")
|
||||
if tag_filter:
|
||||
self.logger.info(f" 标签过滤: {tag_filter}")
|
||||
if priority_filter is not None:
|
||||
self.logger.info(f" 优先级过滤: {priority_filter}")
|
||||
|
||||
result = self.test_engine.execute_test_cases_by_filter(
|
||||
test_cases,
|
||||
module_filter=module_filter,
|
||||
tag_filter=tag_filter,
|
||||
priority_filter=priority_filter
|
||||
)
|
||||
|
||||
if generate_report:
|
||||
self._generate_report(result, report_format, report_path)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"按过滤条件运行测试套件失败: {str(e)}"
|
||||
if self.logger:
|
||||
self.logger.error(error_msg)
|
||||
raise TestRunException(error_msg) from e
|
||||
|
||||
def _generate_report(
|
||||
self,
|
||||
test_suite_result: TestSuiteResult,
|
||||
report_format: str,
|
||||
report_path: Optional[Path]
|
||||
):
|
||||
"""生成测试报告
|
||||
|
||||
Args:
|
||||
test_suite_result: 测试套件结果
|
||||
report_format: 报告格式
|
||||
report_path: 报告输出路径
|
||||
"""
|
||||
try:
|
||||
if report_path is None:
|
||||
report_path = Path(f"reports/test_report_{test_suite_result.suite_name}_{test_suite_result.start_time.strftime('%Y%m%d_%H%M%S')}.{report_format}")
|
||||
|
||||
if report_format == "html":
|
||||
self.report_manager.generate_html_report(
|
||||
test_suite_result,
|
||||
report_path,
|
||||
title="API测试报告"
|
||||
)
|
||||
elif report_format == "json":
|
||||
self.report_manager.generate_json_report(
|
||||
test_suite_result,
|
||||
report_path
|
||||
)
|
||||
else:
|
||||
if self.logger:
|
||||
self.logger.warning(f"不支持的报告格式: {report_format}")
|
||||
|
||||
except Exception as e:
|
||||
if self.logger:
|
||||
self.logger.error(f"生成测试报告失败: {str(e)}")
|
||||
|
||||
def set_base_url(self, base_url: str):
|
||||
"""设置基础URL
|
||||
|
||||
Args:
|
||||
base_url: 基础URL
|
||||
"""
|
||||
self.api_client.base_url = base_url
|
||||
if self.logger:
|
||||
self.logger.info(f"基础URL已更新: {base_url}")
|
||||
|
||||
def set_auth_token(self, token: str):
|
||||
"""设置认证令牌
|
||||
|
||||
Args:
|
||||
token: 认证令牌
|
||||
"""
|
||||
self.auth_manager.set_token(token)
|
||||
if self.logger:
|
||||
self.logger.info("认证令牌已设置")
|
||||
@@ -0,0 +1,5 @@
|
||||
"""报告模块"""
|
||||
|
||||
from apitest.report.report_manager import ReportManager
|
||||
|
||||
__all__ = ["ReportManager"]
|
||||
@@ -0,0 +1,343 @@
|
||||
"""报告管理器模块"""
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from apitest.models.test_models import TestSuiteResult
|
||||
from apitest.models.exceptions import ReportException
|
||||
|
||||
|
||||
class ReportManager:
|
||||
"""报告管理器"""
|
||||
|
||||
def __init__(self, logger=None):
|
||||
"""初始化报告管理器
|
||||
|
||||
Args:
|
||||
logger: 日志记录器
|
||||
"""
|
||||
self.logger = logger
|
||||
|
||||
def generate_html_report(
|
||||
self,
|
||||
test_suite_result: TestSuiteResult,
|
||||
output_path: Path,
|
||||
title: str = "API测试报告"
|
||||
) -> str:
|
||||
"""生成HTML格式的测试报告
|
||||
|
||||
Args:
|
||||
test_suite_result: 测试套件结果
|
||||
output_path: 输出文件路径
|
||||
title: 报告标题
|
||||
|
||||
Returns:
|
||||
生成的报告文件路径
|
||||
|
||||
Raises:
|
||||
ReportException: 报告生成失败
|
||||
"""
|
||||
try:
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
html_content = self._generate_html_content(
|
||||
test_suite_result,
|
||||
title
|
||||
)
|
||||
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(html_content)
|
||||
|
||||
if self.logger:
|
||||
self.logger.info(f"HTML报告已生成: {output_path}")
|
||||
|
||||
return str(output_path)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"生成HTML报告失败: {str(e)}"
|
||||
if self.logger:
|
||||
self.logger.error(error_msg)
|
||||
raise ReportException(error_msg) from e
|
||||
|
||||
def generate_json_report(
|
||||
self,
|
||||
test_suite_result: TestSuiteResult,
|
||||
output_path: Path
|
||||
) -> str:
|
||||
"""生成JSON格式的测试报告
|
||||
|
||||
Args:
|
||||
test_suite_result: 测试套件结果
|
||||
output_path: 输出文件路径
|
||||
|
||||
Returns:
|
||||
生成的报告文件路径
|
||||
|
||||
Raises:
|
||||
ReportException: 报告生成失败
|
||||
"""
|
||||
try:
|
||||
import json
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
report_data = {
|
||||
"suite_name": test_suite_result.suite_name,
|
||||
"total": test_suite_result.total,
|
||||
"passed": test_suite_result.passed,
|
||||
"failed": test_suite_result.failed,
|
||||
"skipped": test_suite_result.skipped,
|
||||
"pass_rate": test_suite_result.pass_rate,
|
||||
"duration": test_suite_result.duration,
|
||||
"start_time": test_suite_result.start_time.isoformat(),
|
||||
"end_time": test_suite_result.end_time.isoformat() if test_suite_result.end_time else None,
|
||||
"results": [result.to_dict() for result in test_suite_result.results]
|
||||
}
|
||||
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
json.dump(report_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
if self.logger:
|
||||
self.logger.info(f"JSON报告已生成: {output_path}")
|
||||
|
||||
return str(output_path)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"生成JSON报告失败: {str(e)}"
|
||||
if self.logger:
|
||||
self.logger.error(error_msg)
|
||||
raise ReportException(error_msg) from e
|
||||
|
||||
def _generate_html_content(
|
||||
self,
|
||||
test_suite_result: TestSuiteResult,
|
||||
title: str
|
||||
) -> str:
|
||||
"""生成HTML内容
|
||||
|
||||
Args:
|
||||
test_suite_result: 测试套件结果
|
||||
title: 报告标题
|
||||
|
||||
Returns:
|
||||
HTML内容
|
||||
"""
|
||||
pass_rate = test_suite_result.pass_rate
|
||||
duration = test_suite_result.duration
|
||||
|
||||
pass_color = "#28a745" if pass_rate >= 80 else "#ffc107" if pass_rate >= 60 else "#dc3545"
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{title}</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}}
|
||||
h1 {{
|
||||
color: #333;
|
||||
border-bottom: 3px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
}}
|
||||
.summary {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 30px 0;
|
||||
}}
|
||||
.summary-card {{
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
border: 1px solid #dee2e6;
|
||||
}}
|
||||
.summary-card h3 {{
|
||||
margin: 0 0 10px 0;
|
||||
color: #6c757d;
|
||||
font-size: 14px;
|
||||
}}
|
||||
.summary-card .value {{
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
}}
|
||||
.summary-card .value.passed {{
|
||||
color: #28a745;
|
||||
}}
|
||||
.summary-card .value.failed {{
|
||||
color: #dc3545;
|
||||
}}
|
||||
.summary-card .value.skipped {{
|
||||
color: #ffc107;
|
||||
}}
|
||||
.progress-bar {{
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
.progress-fill {{
|
||||
height: 100%;
|
||||
background-color: {pass_color};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
transition: width 0.3s ease;
|
||||
}}
|
||||
table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
th, td {{
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}}
|
||||
th {{
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}}
|
||||
tr:hover {{
|
||||
background-color: #f8f9fa;
|
||||
}}
|
||||
.status-pass {{
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
}}
|
||||
.status-fail {{
|
||||
color: #dc3545;
|
||||
font-weight: bold;
|
||||
}}
|
||||
.badge {{
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
}}
|
||||
.badge-info {{
|
||||
background-color: #17a2b8;
|
||||
color: white;
|
||||
}}
|
||||
.badge-warning {{
|
||||
background-color: #ffc107;
|
||||
color: #212529;
|
||||
}}
|
||||
.badge-danger {{
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}}
|
||||
.error-message {{
|
||||
color: #dc3545;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}}
|
||||
.timestamp {{
|
||||
color: #6c757d;
|
||||
font-size: 12px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>{title}</h1>
|
||||
<p class="timestamp">测试套件: {test_suite_result.suite_name}</p>
|
||||
<p class="timestamp">生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</p>
|
||||
|
||||
<div class="summary">
|
||||
<div class="summary-card">
|
||||
<h3>总用例数</h3>
|
||||
<div class="value">{test_suite_result.total}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<h3>通过</h3>
|
||||
<div class="value passed">{test_suite_result.passed}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<h3>失败</h3>
|
||||
<div class="value failed">{test_suite_result.failed}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<h3>跳过</h3>
|
||||
<div class="value skipped">{test_suite_result.skipped}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<h3>通过率</h3>
|
||||
<div class="value">{pass_rate:.1f}%</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<h3>执行时长</h3>
|
||||
<div class="value">{duration:.2f}s</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: {pass_rate}%">
|
||||
{pass_rate:.1f}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>测试结果详情</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用例ID</th>
|
||||
<th>用例名称</th>
|
||||
<th>模块</th>
|
||||
<th>状态</th>
|
||||
<th>状态码</th>
|
||||
<th>响应时间</th>
|
||||
<th>错误信息</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
"""
|
||||
|
||||
for result in test_suite_result.results:
|
||||
status_class = "status-pass" if result.passed else "status-fail"
|
||||
status_text = "通过" if result.passed else "失败"
|
||||
error_msg = result.error_message if result.error_message else ""
|
||||
response_time = f"{result.performance.response_time / 1000:.3f}s" if result.performance else "N/A"
|
||||
|
||||
html += f"""
|
||||
<tr>
|
||||
<td>{result.test_case.id}</td>
|
||||
<td>{result.test_case.name}</td>
|
||||
<td>{result.test_case.module}</td>
|
||||
<td class="{status_class}">{status_text}</td>
|
||||
<td>{result.status_code}</td>
|
||||
<td>{response_time}</td>
|
||||
<td class="error-message">{error_msg}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
html += """
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return html
|
||||
@@ -0,0 +1,4 @@
|
||||
userId,username,email
|
||||
1,testuser1,test1@example.com
|
||||
2,testuser2,test2@example.com
|
||||
3,testuser3,test3@example.com
|
||||
|
@@ -0,0 +1,161 @@
|
||||
"""CLI接口单元测试"""
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
from pathlib import Path
|
||||
import json
|
||||
import tempfile
|
||||
from apitest.cli_module import cli
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner():
|
||||
"""创建CLI测试运行器"""
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_test_cases(tmp_path):
|
||||
"""创建示例测试用例文件"""
|
||||
test_data = [
|
||||
{
|
||||
"id": "TC001",
|
||||
"name": "测试用例1",
|
||||
"description": "测试用例1",
|
||||
"module": "test",
|
||||
"endpoint": "/api/test1",
|
||||
"method": "GET",
|
||||
"headers": {},
|
||||
"enabled": True
|
||||
},
|
||||
{
|
||||
"id": "TC002",
|
||||
"name": "测试用例2",
|
||||
"description": "测试用例2",
|
||||
"module": "user",
|
||||
"endpoint": "/api/user",
|
||||
"method": "POST",
|
||||
"headers": {},
|
||||
"enabled": True,
|
||||
"tags": ["smoke", "regression"],
|
||||
"priority": 1
|
||||
}
|
||||
]
|
||||
|
||||
test_file = tmp_path / "test_cases.json"
|
||||
with open(test_file, "w", encoding="utf-8") as f:
|
||||
json.dump(test_data, f)
|
||||
|
||||
return test_file
|
||||
|
||||
|
||||
def test_cli_version(runner):
|
||||
"""测试CLI版本命令"""
|
||||
result = runner.invoke(cli, ["--version"])
|
||||
assert result.exit_code == 0
|
||||
assert "1.0.0" in result.output
|
||||
|
||||
|
||||
def test_cli_help(runner):
|
||||
"""测试CLI帮助命令"""
|
||||
result = runner.invoke(cli, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "黑盒API测试工具" in result.output
|
||||
|
||||
|
||||
def test_list_command(runner, sample_test_cases):
|
||||
"""测试列出测试用例命令"""
|
||||
result = runner.invoke(cli, ["list", str(sample_test_cases)])
|
||||
assert result.exit_code == 0
|
||||
assert "测试用例总数: 2" in result.output
|
||||
assert "TC001" in result.output
|
||||
assert "TC002" in result.output
|
||||
|
||||
|
||||
def test_list_command_with_module_filter(runner, sample_test_cases):
|
||||
"""测试列出测试用例命令(模块过滤)"""
|
||||
result = runner.invoke(cli, ["list", str(sample_test_cases), "--module", "test"])
|
||||
assert result.exit_code == 0
|
||||
assert "TC001" in result.output
|
||||
assert "TC002" not in result.output
|
||||
|
||||
|
||||
def test_list_command_with_tag_filter(runner, sample_test_cases):
|
||||
"""测试列出测试用例命令(标签过滤)"""
|
||||
result = runner.invoke(cli, ["list", str(sample_test_cases), "--tag", "smoke"])
|
||||
assert result.exit_code == 0
|
||||
assert "TC001" not in result.output
|
||||
assert "TC002" in result.output
|
||||
|
||||
|
||||
def test_list_command_with_priority_filter(runner, sample_test_cases):
|
||||
"""测试列出测试用例命令(优先级过滤)"""
|
||||
result = runner.invoke(cli, ["list", str(sample_test_cases), "--priority", "1"])
|
||||
assert result.exit_code == 0
|
||||
assert "TC001" not in result.output
|
||||
assert "TC002" in result.output
|
||||
|
||||
|
||||
def test_validate_command(runner, sample_test_cases):
|
||||
"""测试验证测试用例命令"""
|
||||
result = runner.invoke(cli, ["validate", str(sample_test_cases)])
|
||||
assert result.exit_code == 0
|
||||
assert "验证通过" in result.output
|
||||
|
||||
|
||||
def test_validate_command_invalid_file(runner, tmp_path):
|
||||
"""测试验证测试用例命令(无效文件)"""
|
||||
invalid_file = tmp_path / "invalid.json"
|
||||
with open(invalid_file, "w", encoding="utf-8") as f:
|
||||
f.write("{ invalid json }")
|
||||
|
||||
result = runner.invoke(cli, ["validate", str(invalid_file)])
|
||||
assert result.exit_code != 0
|
||||
|
||||
|
||||
def test_config_command(runner):
|
||||
"""测试配置命令"""
|
||||
result = runner.invoke(cli, ["config"])
|
||||
assert result.exit_code == 0
|
||||
assert "当前配置" in result.output
|
||||
|
||||
|
||||
def test_config_command_with_key(runner):
|
||||
"""测试配置命令(指定键)"""
|
||||
result = runner.invoke(cli, ["config", "--key", "target.base_url"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_run_command_no_test_cases(runner):
|
||||
"""测试运行命令(无测试用例)"""
|
||||
result = runner.invoke(cli, ["run"])
|
||||
assert result.exit_code != 0
|
||||
assert "请指定测试用例文件路径" in result.output
|
||||
|
||||
|
||||
def test_run_command_help(runner):
|
||||
"""测试运行命令帮助"""
|
||||
result = runner.invoke(cli, ["run", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "运行测试用例" in result.output
|
||||
|
||||
|
||||
def test_list_command_help(runner):
|
||||
"""测试列出命令帮助"""
|
||||
result = runner.invoke(cli, ["list", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "列出测试用例" in result.output
|
||||
|
||||
|
||||
def test_validate_command_help(runner):
|
||||
"""测试验证命令帮助"""
|
||||
result = runner.invoke(cli, ["validate", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "验证测试用例文件" in result.output
|
||||
|
||||
|
||||
def test_config_command_help(runner):
|
||||
"""测试配置命令帮助"""
|
||||
result = runner.invoke(cli, ["config", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "查看配置" in result.output
|
||||
@@ -0,0 +1,224 @@
|
||||
import pytest
|
||||
import yaml
|
||||
import os
|
||||
from pathlib import Path
|
||||
from apitest.config.config_manager import ConfigManager
|
||||
from apitest.models.exceptions import ConfigException
|
||||
|
||||
|
||||
class TestConfigManager:
|
||||
"""测试ConfigManager配置管理器"""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_config_file(self, tmp_path):
|
||||
"""创建临时配置文件"""
|
||||
config_data = {
|
||||
"target": {
|
||||
"base_url": "http://localhost:8080",
|
||||
"timeout": 5000,
|
||||
"max_retries": 3
|
||||
},
|
||||
"auth": {
|
||||
"username": "test_user",
|
||||
"password": "test_pass",
|
||||
"login_endpoint": "/sys/auth/login"
|
||||
},
|
||||
"test": {
|
||||
"data_dir": "data",
|
||||
"test_cases_dir": "test_cases",
|
||||
"parallel": True,
|
||||
"parallel_threads": 4,
|
||||
"retry_count": 2,
|
||||
"stop_on_failure": False,
|
||||
"max_response_time": 5000
|
||||
},
|
||||
"report": {
|
||||
"output_dir": "reports",
|
||||
"format": "html"
|
||||
},
|
||||
"logging": {
|
||||
"level": "INFO",
|
||||
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
"file": "logs/api_test.log"
|
||||
}
|
||||
}
|
||||
|
||||
config_file = tmp_path / "config.yaml"
|
||||
with open(config_file, "w", encoding="utf-8") as f:
|
||||
yaml.dump(config_data, f)
|
||||
|
||||
return config_file
|
||||
|
||||
def test_config_manager_initialization(self, temp_config_file):
|
||||
"""测试配置管理器初始化"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
assert config_manager.config_path == temp_config_file
|
||||
assert config_manager._config is not None
|
||||
|
||||
def test_config_manager_nonexistent_file(self):
|
||||
"""测试配置文件不存在"""
|
||||
with pytest.raises(ConfigException, match="配置文件不存在"):
|
||||
ConfigManager("/nonexistent/config.yaml")
|
||||
|
||||
def test_get_config_value(self, temp_config_file):
|
||||
"""测试获取配置值"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
|
||||
assert config_manager.get("target.base_url") == "http://localhost:8080"
|
||||
assert config_manager.get("target.timeout") == 5000
|
||||
assert config_manager.get("target.max_retries") == 3
|
||||
|
||||
def test_get_nested_config_value(self, temp_config_file):
|
||||
"""测试获取嵌套配置值"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
|
||||
assert config_manager.get("auth.username") == "test_user"
|
||||
assert config_manager.get("auth.password") == "test_pass"
|
||||
assert config_manager.get("auth.login_endpoint") == "/sys/auth/login"
|
||||
|
||||
def test_get_config_value_with_default(self, temp_config_file):
|
||||
"""测试获取配置值(带默认值)"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
|
||||
assert config_manager.get("nonexistent.key", "default_value") == "default_value"
|
||||
assert config_manager.get("target.nonexistent", 0) == 0
|
||||
|
||||
def test_get_target_config(self, temp_config_file):
|
||||
"""测试获取目标系统配置"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
target_config = config_manager.get_target_config()
|
||||
|
||||
assert target_config["base_url"] == "http://localhost:8080"
|
||||
assert target_config["timeout"] == 5000
|
||||
assert target_config["max_retries"] == 3
|
||||
|
||||
def test_get_auth_config(self, temp_config_file):
|
||||
"""测试获取认证配置"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
auth_config = config_manager.get_auth_config()
|
||||
|
||||
assert auth_config["username"] == "test_user"
|
||||
assert auth_config["password"] == "test_pass"
|
||||
assert auth_config["login_endpoint"] == "/sys/auth/login"
|
||||
|
||||
def test_get_test_config(self, temp_config_file):
|
||||
"""测试获取测试配置"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
test_config = config_manager.get_test_config()
|
||||
|
||||
assert test_config["data_dir"] == "data"
|
||||
assert test_config["test_cases_dir"] == "test_cases"
|
||||
assert test_config["parallel"] == True
|
||||
assert test_config["parallel_threads"] == 4
|
||||
|
||||
def test_get_report_config(self, temp_config_file):
|
||||
"""测试获取报告配置"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
report_config = config_manager.get_report_config()
|
||||
|
||||
assert report_config["output_dir"] == "reports"
|
||||
assert report_config["format"] == "html"
|
||||
|
||||
def test_get_logging_config(self, temp_config_file):
|
||||
"""测试获取日志配置"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
logging_config = config_manager.get_logging_config()
|
||||
|
||||
assert logging_config["level"] == "INFO"
|
||||
assert logging_config["file"] == "logs/api_test.log"
|
||||
|
||||
def test_get_base_url(self, temp_config_file):
|
||||
"""测试获取基础URL"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
assert config_manager.get_base_url() == "http://localhost:8080"
|
||||
|
||||
def test_get_timeout(self, temp_config_file):
|
||||
"""测试获取超时时间"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
assert config_manager.get_timeout() == 5000
|
||||
|
||||
def test_get_max_retries(self, temp_config_file):
|
||||
"""测试获取最大重试次数"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
assert config_manager.get_max_retries() == 3
|
||||
|
||||
def test_get_login_endpoint(self, temp_config_file):
|
||||
"""测试获取登录端点"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
assert config_manager.get_login_endpoint() == "/sys/auth/login"
|
||||
|
||||
def test_is_parallel_enabled(self, temp_config_file):
|
||||
"""测试是否启用并行执行"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
assert config_manager.is_parallel_enabled() == True
|
||||
|
||||
def test_get_parallel_threads(self, temp_config_file):
|
||||
"""测试获取并行线程数"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
assert config_manager.get_parallel_threads() == 4
|
||||
|
||||
def test_get_retry_count(self, temp_config_file):
|
||||
"""测试获取重试次数"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
assert config_manager.get_retry_count() == 2
|
||||
|
||||
def test_should_stop_on_failure(self, temp_config_file):
|
||||
"""测试是否在失败时停止"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
assert config_manager.should_stop_on_failure() == False
|
||||
|
||||
def test_get_max_response_time(self, temp_config_file):
|
||||
"""测试获取最大响应时间"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
assert config_manager.get_max_response_time() == 5000
|
||||
|
||||
def test_get_report_format(self, temp_config_file):
|
||||
"""测试获取报告格式"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
assert config_manager.get_report_format() == "html"
|
||||
|
||||
def test_get_log_level(self, temp_config_file):
|
||||
"""测试获取日志级别"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
assert config_manager.get_log_level() == "INFO"
|
||||
|
||||
def test_get_log_format(self, temp_config_file):
|
||||
"""测试获取日志格式"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
expected_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
assert config_manager.get_log_format() == expected_format
|
||||
|
||||
def test_get_log_file(self, temp_config_file):
|
||||
"""测试获取日志文件路径"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
log_file = config_manager.get_log_file()
|
||||
assert log_file is not None
|
||||
assert log_file.name == "api_test.log"
|
||||
|
||||
def test_get_auth_credentials_with_env(self, temp_config_file, monkeypatch):
|
||||
"""测试获取认证凭据(环境变量)"""
|
||||
monkeypatch.setenv("TEST_USERNAME", "env_user")
|
||||
monkeypatch.setenv("TEST_PASSWORD", "env_pass")
|
||||
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
credentials = config_manager.get_auth_credentials()
|
||||
|
||||
assert credentials["username"] == "env_user"
|
||||
assert credentials["password"] == "env_pass"
|
||||
|
||||
def test_reload_config(self, temp_config_file):
|
||||
"""测试重新加载配置"""
|
||||
config_manager = ConfigManager(str(temp_config_file))
|
||||
|
||||
original_url = config_manager.get_base_url()
|
||||
assert original_url == "http://localhost:8080"
|
||||
|
||||
with open(temp_config_file, "r+", encoding="utf-8") as f:
|
||||
config_data = yaml.safe_load(f)
|
||||
config_data["target"]["base_url"] = "http://new-url:8080"
|
||||
f.seek(0)
|
||||
yaml.dump(config_data, f)
|
||||
f.truncate()
|
||||
|
||||
config_manager.reload()
|
||||
assert config_manager.get_base_url() == "http://new-url:8080"
|
||||
@@ -0,0 +1,137 @@
|
||||
import pytest
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
from apitest.config.config_manager import ConfigManager
|
||||
from apitest.config.logger_manager import LoggerManager
|
||||
|
||||
|
||||
class TestLoggerManager:
|
||||
"""测试LoggerManager日志管理器"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_manager(self):
|
||||
"""模拟配置管理器"""
|
||||
config_manager = MagicMock(spec=ConfigManager)
|
||||
config_manager.get_log_level.return_value = "INFO"
|
||||
config_manager.get_log_format.return_value = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
config_manager.get_log_file.return_value = None
|
||||
return config_manager
|
||||
|
||||
@pytest.fixture
|
||||
def logger_manager(self, mock_config_manager):
|
||||
"""创建日志管理器实例"""
|
||||
return LoggerManager(mock_config_manager)
|
||||
|
||||
def test_logger_manager_initialization(self, mock_config_manager):
|
||||
"""测试日志管理器初始化"""
|
||||
logger_manager = LoggerManager(mock_config_manager)
|
||||
assert logger_manager.config_manager == mock_config_manager
|
||||
assert isinstance(logger_manager._loggers, dict)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_manager_with_file(self, tmp_path):
|
||||
"""模拟带日志文件的配置管理器"""
|
||||
config_manager = MagicMock(spec=ConfigManager)
|
||||
config_manager.get_log_level.return_value = "DEBUG"
|
||||
config_manager.get_log_format.return_value = "%(name)s - %(levelname)s - %(message)s"
|
||||
config_manager.get_log_file.return_value = tmp_path / "test.log"
|
||||
return config_manager
|
||||
|
||||
def test_logger_manager_with_file(self, mock_config_manager_with_file):
|
||||
"""测试带日志文件的日志管理器初始化"""
|
||||
logger_manager = LoggerManager(mock_config_manager_with_file)
|
||||
assert logger_manager.config_manager == mock_config_manager_with_file
|
||||
|
||||
def test_get_logger(self, logger_manager):
|
||||
"""测试获取日志记录器"""
|
||||
logger = logger_manager.get_logger("test_logger")
|
||||
assert isinstance(logger, logging.Logger)
|
||||
assert logger.name == "test_logger"
|
||||
assert "test_logger" in logger_manager._loggers
|
||||
|
||||
def test_get_logger_cached(self, logger_manager):
|
||||
"""测试获取缓存的日志记录器"""
|
||||
logger1 = logger_manager.get_logger("test_logger")
|
||||
logger2 = logger_manager.get_logger("test_logger")
|
||||
assert logger1 is logger2
|
||||
|
||||
def test_set_level(self, logger_manager):
|
||||
"""测试设置日志级别"""
|
||||
logger_manager.set_level("DEBUG")
|
||||
root_logger = logging.getLogger()
|
||||
assert root_logger.level == logging.DEBUG
|
||||
|
||||
def test_add_file_handler(self, logger_manager, tmp_path):
|
||||
"""测试添加文件处理器"""
|
||||
log_file = tmp_path / "test_add_handler.log"
|
||||
|
||||
initial_handler_count = len([h for h in logging.getLogger().handlers if isinstance(h, logging.FileHandler)])
|
||||
|
||||
logger_manager.add_file_handler(log_file)
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
file_handlers = [h for h in root_logger.handlers if isinstance(h, logging.FileHandler)]
|
||||
assert len(file_handlers) > initial_handler_count
|
||||
|
||||
new_handlers = file_handlers[initial_handler_count:]
|
||||
assert len(new_handlers) > 0
|
||||
assert str(log_file) in new_handlers[0].baseFilename
|
||||
|
||||
def test_add_file_handler_with_level(self, logger_manager, tmp_path):
|
||||
"""测试添加带级别的文件处理器"""
|
||||
log_file = tmp_path / "test_add_handler_level.log"
|
||||
|
||||
initial_handler_count = len([h for h in logging.getLogger().handlers if isinstance(h, logging.FileHandler)])
|
||||
|
||||
logger_manager.add_file_handler(log_file, "ERROR")
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
file_handlers = [h for h in root_logger.handlers if isinstance(h, logging.FileHandler)]
|
||||
assert len(file_handlers) > initial_handler_count
|
||||
|
||||
new_handlers = file_handlers[initial_handler_count:]
|
||||
assert len(new_handlers) > 0
|
||||
assert new_handlers[0].level == logging.ERROR
|
||||
|
||||
def test_remove_all_handlers(self, logger_manager):
|
||||
"""测试移除所有处理器"""
|
||||
original_handler_count = len(logging.getLogger().handlers)
|
||||
logger_manager.remove_all_handlers()
|
||||
assert len(logging.getLogger().handlers) == 0
|
||||
|
||||
@patch('apitest.config.logger_manager.setup_logger')
|
||||
def test_setup_logger_function(self, mock_setup):
|
||||
"""测试setup_logger函数"""
|
||||
from apitest.config.logger_manager import setup_logger
|
||||
mock_config = MagicMock(spec=ConfigManager)
|
||||
setup_logger(mock_config)
|
||||
mock_setup.assert_called_once_with(mock_config)
|
||||
|
||||
|
||||
class TestSetupLoggerIntegration:
|
||||
"""测试setup_logger集成"""
|
||||
|
||||
def test_setup_logger_creates_logger_manager(self):
|
||||
"""测试setup_logger创建日志管理器"""
|
||||
config_data = {
|
||||
"logging": {
|
||||
"level": "INFO",
|
||||
"format": "%(name)s - %(levelname)s - %(message)s",
|
||||
"file": None
|
||||
}
|
||||
}
|
||||
|
||||
with patch('apitest.config.config_manager.ConfigManager') as mock_config_class:
|
||||
mock_config = MagicMock(spec=ConfigManager)
|
||||
mock_config.get_log_level.return_value = "INFO"
|
||||
mock_config.get_log_format.return_value = "%(name)s - %(levelname)s - %(message)s"
|
||||
mock_config.get_log_file.return_value = None
|
||||
mock_config_class.return_value = mock_config
|
||||
|
||||
from apitest.config.logger_manager import setup_logger
|
||||
logger_manager = setup_logger(mock_config)
|
||||
|
||||
assert isinstance(logger_manager, LoggerManager)
|
||||
assert logger_manager.config_manager == mock_config
|
||||
@@ -0,0 +1,378 @@
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from apitest.models.test_models import (
|
||||
HTTPMethod,
|
||||
ValidationRule,
|
||||
TestCase,
|
||||
PerformanceMetrics,
|
||||
TestResult,
|
||||
TestSuiteResult
|
||||
)
|
||||
from apitest.models.exceptions import (
|
||||
APITestException,
|
||||
ConfigException,
|
||||
DataException,
|
||||
AuthException,
|
||||
RequestException,
|
||||
ValidationException,
|
||||
TestRunException,
|
||||
ReportException
|
||||
)
|
||||
|
||||
|
||||
class TestHTTPMethod:
|
||||
"""测试HTTPMethod枚举"""
|
||||
|
||||
def test_http_methods(self):
|
||||
"""测试所有HTTP方法"""
|
||||
assert HTTPMethod.GET.value == "GET"
|
||||
assert HTTPMethod.POST.value == "POST"
|
||||
assert HTTPMethod.PUT.value == "PUT"
|
||||
assert HTTPMethod.DELETE.value == "DELETE"
|
||||
assert HTTPMethod.PATCH.value == "PATCH"
|
||||
assert HTTPMethod.HEAD.value == "HEAD"
|
||||
assert HTTPMethod.OPTIONS.value == "OPTIONS"
|
||||
|
||||
|
||||
class TestValidationRule:
|
||||
"""测试ValidationRule数据类"""
|
||||
|
||||
def test_validation_rule_creation(self):
|
||||
"""测试创建验证规则"""
|
||||
rule = ValidationRule(
|
||||
type="status_code",
|
||||
expected=200,
|
||||
message="状态码应为200"
|
||||
)
|
||||
assert rule.type == "status_code"
|
||||
assert rule.expected == 200
|
||||
assert rule.message == "状态码应为200"
|
||||
assert rule.json_path is None
|
||||
|
||||
def test_validation_rule_with_json_path(self):
|
||||
"""测试带JSON路径的验证规则"""
|
||||
rule = ValidationRule(
|
||||
type="json_path",
|
||||
expected="success",
|
||||
json_path="$.status",
|
||||
message="状态应为success"
|
||||
)
|
||||
assert rule.json_path == "$.status"
|
||||
|
||||
def test_validation_rule_immutability(self):
|
||||
"""测试验证规则的不可变性"""
|
||||
rule = ValidationRule(type="status_code", expected=200)
|
||||
with pytest.raises(Exception):
|
||||
rule.expected = 201
|
||||
|
||||
|
||||
class TestTestCaseModel:
|
||||
"""测试TestCase数据类"""
|
||||
|
||||
def test_test_case_creation(self):
|
||||
"""测试创建测试用例"""
|
||||
test_case = TestCase(
|
||||
id="TC001",
|
||||
name="测试用户登录",
|
||||
description="验证用户登录功能",
|
||||
module="user",
|
||||
endpoint="/api/auth/login",
|
||||
method=HTTPMethod.POST,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body={"username": "admin", "password": "admin123"}
|
||||
)
|
||||
assert test_case.id == "TC001"
|
||||
assert test_case.name == "测试用户登录"
|
||||
assert test_case.method == HTTPMethod.POST
|
||||
assert test_case.auth_required == True
|
||||
assert test_case.enabled == True
|
||||
assert test_case.dependencies == []
|
||||
assert test_case.validations == []
|
||||
assert test_case.tags == []
|
||||
|
||||
def test_test_case_with_dependencies(self):
|
||||
"""测试带依赖的测试用例"""
|
||||
test_case = TestCase(
|
||||
id="TC002",
|
||||
name="测试获取用户信息",
|
||||
description="验证获取用户信息功能",
|
||||
module="user",
|
||||
endpoint="/api/user/info",
|
||||
method=HTTPMethod.GET,
|
||||
headers={"Content-Type": "application/json"},
|
||||
dependencies=["TC001"]
|
||||
)
|
||||
assert len(test_case.dependencies) == 1
|
||||
assert "TC001" in test_case.dependencies
|
||||
|
||||
def test_test_case_with_validations(self):
|
||||
"""测试带验证规则的测试用例"""
|
||||
test_case = TestCase(
|
||||
id="TC003",
|
||||
name="测试创建用户",
|
||||
description="验证创建用户功能",
|
||||
module="user",
|
||||
endpoint="/api/user",
|
||||
method=HTTPMethod.POST,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body={"username": "test", "password": "test123"},
|
||||
validations=[
|
||||
{"type": "status_code", "expected": 201},
|
||||
{"type": "json_path", "json_path": "$.success", "expected": True}
|
||||
]
|
||||
)
|
||||
assert len(test_case.validations) == 2
|
||||
assert test_case.validations[0]["type"] == "status_code"
|
||||
|
||||
def test_test_case_immutability(self):
|
||||
"""测试测试用例的不可变性"""
|
||||
test_case = TestCase(
|
||||
id="TC004",
|
||||
name="测试用例",
|
||||
description="测试",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={}
|
||||
)
|
||||
with pytest.raises(Exception):
|
||||
test_case.name = "修改后的名称"
|
||||
|
||||
|
||||
class TestPerformanceMetrics:
|
||||
"""测试PerformanceMetrics数据类"""
|
||||
|
||||
def test_performance_metrics_creation(self):
|
||||
"""测试创建性能指标"""
|
||||
metrics = PerformanceMetrics(
|
||||
response_time=500,
|
||||
request_size=100,
|
||||
response_size=200,
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
assert metrics.response_time == 500
|
||||
assert metrics.request_size == 100
|
||||
assert metrics.response_size == 200
|
||||
|
||||
def test_performance_metrics_to_dict(self):
|
||||
"""测试性能指标转换为字典"""
|
||||
timestamp = datetime.now()
|
||||
metrics = PerformanceMetrics(
|
||||
response_time=500,
|
||||
request_size=100,
|
||||
response_size=200,
|
||||
timestamp=timestamp
|
||||
)
|
||||
result = metrics.to_dict()
|
||||
assert result["response_time"] == 500
|
||||
assert result["request_size"] == 100
|
||||
assert result["response_size"] == 200
|
||||
assert result["timestamp"] == timestamp.isoformat()
|
||||
|
||||
|
||||
class TestTestResultModel:
|
||||
"""测试TestResult数据类"""
|
||||
|
||||
def test_test_result_creation(self):
|
||||
"""测试创建测试结果"""
|
||||
test_case = TestCase(
|
||||
id="TC001",
|
||||
name="测试用例",
|
||||
description="测试",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={}
|
||||
)
|
||||
result = TestResult(
|
||||
test_case=test_case,
|
||||
passed=True,
|
||||
status_code=200,
|
||||
response_body={"success": True},
|
||||
response_headers={"Content-Type": "application/json"}
|
||||
)
|
||||
assert result.passed == True
|
||||
assert result.status_code == 200
|
||||
assert result.execution_time == 0.0
|
||||
assert result.retry_count == 0
|
||||
assert result.timestamp is not None
|
||||
|
||||
def test_test_result_with_performance(self):
|
||||
"""测试带性能指标的测试结果"""
|
||||
test_case = TestCase(
|
||||
id="TC001",
|
||||
name="测试用例",
|
||||
description="测试",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={}
|
||||
)
|
||||
performance = PerformanceMetrics(
|
||||
response_time=500,
|
||||
request_size=100,
|
||||
response_size=200,
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
result = TestResult(
|
||||
test_case=test_case,
|
||||
passed=True,
|
||||
status_code=200,
|
||||
response_body={"success": True},
|
||||
response_headers={},
|
||||
performance=performance,
|
||||
execution_time=0.5
|
||||
)
|
||||
assert result.performance == performance
|
||||
assert result.execution_time == 0.5
|
||||
|
||||
def test_test_result_to_dict(self):
|
||||
"""测试测试结果转换为字典"""
|
||||
test_case = TestCase(
|
||||
id="TC001",
|
||||
name="测试用例",
|
||||
description="测试",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={}
|
||||
)
|
||||
result = TestResult(
|
||||
test_case=test_case,
|
||||
passed=True,
|
||||
status_code=200,
|
||||
response_body={"success": True},
|
||||
response_headers={}
|
||||
)
|
||||
result_dict = result.to_dict()
|
||||
assert result_dict["test_case_id"] == "TC001"
|
||||
assert result_dict["test_case_name"] == "测试用例"
|
||||
assert result_dict["passed"] == True
|
||||
assert result_dict["status_code"] == 200
|
||||
|
||||
|
||||
class TestTestSuiteResultModel:
|
||||
"""测试TestSuiteResult数据类"""
|
||||
|
||||
def test_test_suite_result_creation(self):
|
||||
"""测试创建测试套件结果"""
|
||||
suite_result = TestSuiteResult(
|
||||
suite_name="user",
|
||||
total=10,
|
||||
passed=8,
|
||||
failed=1,
|
||||
skipped=1,
|
||||
results=[],
|
||||
start_time=datetime.now()
|
||||
)
|
||||
assert suite_result.suite_name == "user"
|
||||
assert suite_result.total == 10
|
||||
assert suite_result.passed == 8
|
||||
assert suite_result.failed == 1
|
||||
assert suite_result.skipped == 1
|
||||
assert suite_result.end_time is None
|
||||
|
||||
def test_test_suite_result_duration(self):
|
||||
"""测试测试套件执行时长"""
|
||||
start_time = datetime.now()
|
||||
end_time = datetime.now()
|
||||
suite_result = TestSuiteResult(
|
||||
suite_name="user",
|
||||
total=10,
|
||||
passed=8,
|
||||
failed=1,
|
||||
skipped=1,
|
||||
results=[],
|
||||
start_time=start_time,
|
||||
end_time=end_time
|
||||
)
|
||||
assert suite_result.duration >= 0
|
||||
|
||||
def test_test_suite_result_pass_rate(self):
|
||||
"""测试测试套件通过率"""
|
||||
suite_result = TestSuiteResult(
|
||||
suite_name="user",
|
||||
total=10,
|
||||
passed=8,
|
||||
failed=1,
|
||||
skipped=1,
|
||||
results=[],
|
||||
start_time=datetime.now()
|
||||
)
|
||||
assert suite_result.pass_rate == 80.0
|
||||
|
||||
def test_test_suite_result_pass_rate_zero_total(self):
|
||||
"""测试总数为0时的通过率"""
|
||||
suite_result = TestSuiteResult(
|
||||
suite_name="user",
|
||||
total=0,
|
||||
passed=0,
|
||||
failed=0,
|
||||
skipped=0,
|
||||
results=[],
|
||||
start_time=datetime.now()
|
||||
)
|
||||
assert suite_result.pass_rate == 0.0
|
||||
|
||||
def test_test_suite_result_to_dict(self):
|
||||
"""测试测试套件结果转换为字典"""
|
||||
suite_result = TestSuiteResult(
|
||||
suite_name="user",
|
||||
total=10,
|
||||
passed=8,
|
||||
failed=1,
|
||||
skipped=1,
|
||||
results=[],
|
||||
start_time=datetime.now()
|
||||
)
|
||||
result_dict = suite_result.to_dict()
|
||||
assert result_dict["suite_name"] == "user"
|
||||
assert result_dict["total"] == 10
|
||||
assert result_dict["passed"] == 8
|
||||
assert result_dict["failed"] == 1
|
||||
assert result_dict["skipped"] == 1
|
||||
assert result_dict["pass_rate"] == 80.0
|
||||
|
||||
|
||||
class TestExceptions:
|
||||
"""测试异常类"""
|
||||
|
||||
def test_api_test_exception(self):
|
||||
"""测试API测试基础异常"""
|
||||
with pytest.raises(APITestException):
|
||||
raise APITestException("测试异常")
|
||||
|
||||
def test_config_exception(self):
|
||||
"""测试配置异常"""
|
||||
with pytest.raises(ConfigException):
|
||||
raise ConfigException("配置错误")
|
||||
|
||||
def test_data_exception(self):
|
||||
"""测试数据异常"""
|
||||
with pytest.raises(DataException):
|
||||
raise DataException("数据错误")
|
||||
|
||||
def test_auth_exception(self):
|
||||
"""测试认证异常"""
|
||||
with pytest.raises(AuthException):
|
||||
raise AuthException("认证失败")
|
||||
|
||||
def test_request_exception(self):
|
||||
"""测试请求异常"""
|
||||
with pytest.raises(RequestException):
|
||||
raise RequestException("请求失败")
|
||||
|
||||
def test_validation_exception(self):
|
||||
"""测试验证异常"""
|
||||
with pytest.raises(ValidationException):
|
||||
raise ValidationException("验证失败")
|
||||
|
||||
def test_test_execution_exception(self):
|
||||
"""测试测试执行异常"""
|
||||
with pytest.raises(TestRunException):
|
||||
raise TestRunException("执行失败")
|
||||
|
||||
def test_report_exception(self):
|
||||
"""测试报告生成异常"""
|
||||
with pytest.raises(ReportException):
|
||||
raise ReportException("报告生成失败")
|
||||
@@ -0,0 +1,355 @@
|
||||
"""报告管理器单元测试"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from unittest.mock import Mock, patch
|
||||
from apitest.report.report_manager import ReportManager
|
||||
from apitest.models.test_models import TestCase, TestResult, TestSuiteResult, HTTPMethod, PerformanceMetrics
|
||||
from apitest.models.exceptions import ReportException
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def report_manager():
|
||||
"""创建报告管理器实例"""
|
||||
logger = Mock()
|
||||
return ReportManager(logger)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_suite_result():
|
||||
"""创建测试套件结果"""
|
||||
test_cases = [
|
||||
TestCase(
|
||||
id="TC001",
|
||||
name="测试用例1",
|
||||
description="测试用例1",
|
||||
module="test",
|
||||
endpoint="/api/test1",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
enabled=True
|
||||
),
|
||||
TestCase(
|
||||
id="TC002",
|
||||
name="测试用例2",
|
||||
description="测试用例2",
|
||||
module="test",
|
||||
endpoint="/api/test2",
|
||||
method=HTTPMethod.POST,
|
||||
headers={},
|
||||
enabled=True
|
||||
)
|
||||
]
|
||||
|
||||
results = [
|
||||
TestResult(
|
||||
test_case=test_cases[0],
|
||||
passed=True,
|
||||
status_code=200,
|
||||
response_body={"message": "success"},
|
||||
response_headers={"Content-Type": "application/json"},
|
||||
performance=PerformanceMetrics(
|
||||
timestamp=datetime.now(),
|
||||
response_time=123,
|
||||
request_size=0,
|
||||
response_size=0
|
||||
)
|
||||
),
|
||||
TestResult(
|
||||
test_case=test_cases[1],
|
||||
passed=False,
|
||||
status_code=500,
|
||||
response_body={"error": "internal error"},
|
||||
response_headers={"Content-Type": "application/json"},
|
||||
performance=PerformanceMetrics(
|
||||
timestamp=datetime.now(),
|
||||
response_time=456,
|
||||
request_size=0,
|
||||
response_size=0
|
||||
),
|
||||
error_message="服务器内部错误"
|
||||
)
|
||||
]
|
||||
|
||||
return TestSuiteResult(
|
||||
suite_name="Test Suite",
|
||||
total=2,
|
||||
passed=1,
|
||||
failed=1,
|
||||
skipped=0,
|
||||
results=results,
|
||||
start_time=datetime.now()
|
||||
)
|
||||
|
||||
|
||||
def test_generate_html_report_success(report_manager, test_suite_result, tmp_path):
|
||||
"""测试生成HTML报告(成功)"""
|
||||
report_path = tmp_path / "test_report.html"
|
||||
|
||||
result_path = report_manager.generate_html_report(
|
||||
test_suite_result,
|
||||
report_path,
|
||||
title="测试报告"
|
||||
)
|
||||
|
||||
assert result_path == str(report_path)
|
||||
assert report_path.exists()
|
||||
|
||||
content = report_path.read_text(encoding="utf-8")
|
||||
assert "测试报告" in content
|
||||
assert "Test Suite" in content
|
||||
assert "TC001" in content
|
||||
assert "TC002" in content
|
||||
assert "测试用例1" in content
|
||||
assert "测试用例2" in content
|
||||
assert "50.0%" in content or "50.0" in content
|
||||
|
||||
|
||||
def test_generate_html_report_create_directory(report_manager, test_suite_result, tmp_path):
|
||||
"""测试生成HTML报告(创建目录)"""
|
||||
report_path = tmp_path / "reports" / "nested" / "test_report.html"
|
||||
|
||||
result_path = report_manager.generate_html_report(
|
||||
test_suite_result,
|
||||
report_path
|
||||
)
|
||||
|
||||
assert result_path == str(report_path)
|
||||
assert report_path.exists()
|
||||
assert report_path.parent.exists()
|
||||
|
||||
|
||||
def test_generate_json_report_success(report_manager, test_suite_result, tmp_path):
|
||||
"""测试生成JSON报告(成功)"""
|
||||
report_path = tmp_path / "test_report.json"
|
||||
|
||||
result_path = report_manager.generate_json_report(
|
||||
test_suite_result,
|
||||
report_path
|
||||
)
|
||||
|
||||
assert result_path == str(report_path)
|
||||
assert report_path.exists()
|
||||
|
||||
import json
|
||||
content = report_path.read_text(encoding="utf-8")
|
||||
data = json.loads(content)
|
||||
|
||||
assert data["suite_name"] == "Test Suite"
|
||||
assert data["total"] == 2
|
||||
assert data["passed"] == 1
|
||||
assert data["failed"] == 1
|
||||
assert data["skipped"] == 0
|
||||
assert len(data["results"]) == 2
|
||||
|
||||
|
||||
def test_generate_json_report_create_directory(report_manager, test_suite_result, tmp_path):
|
||||
"""测试生成JSON报告(创建目录)"""
|
||||
report_path = tmp_path / "reports" / "nested" / "test_report.json"
|
||||
|
||||
result_path = report_manager.generate_json_report(
|
||||
test_suite_result,
|
||||
report_path
|
||||
)
|
||||
|
||||
assert result_path == str(report_path)
|
||||
assert report_path.exists()
|
||||
assert report_path.parent.exists()
|
||||
|
||||
|
||||
def test_generate_html_report_logger_call(report_manager, test_suite_result, tmp_path):
|
||||
"""测试生成HTML报告时调用日志记录器"""
|
||||
report_path = tmp_path / "test_report.html"
|
||||
|
||||
report_manager.generate_html_report(
|
||||
test_suite_result,
|
||||
report_path
|
||||
)
|
||||
|
||||
report_manager.logger.info.assert_called()
|
||||
|
||||
|
||||
def test_generate_json_report_logger_call(report_manager, test_suite_result, tmp_path):
|
||||
"""测试生成JSON报告时调用日志记录器"""
|
||||
report_path = tmp_path / "test_report.json"
|
||||
|
||||
report_manager.generate_json_report(
|
||||
test_suite_result,
|
||||
report_path
|
||||
)
|
||||
|
||||
report_manager.logger.info.assert_called()
|
||||
|
||||
|
||||
def test_generate_html_report_content_structure(report_manager, test_suite_result, tmp_path):
|
||||
"""测试HTML报告内容结构"""
|
||||
report_path = tmp_path / "test_report.html"
|
||||
|
||||
report_manager.generate_html_report(
|
||||
test_suite_result,
|
||||
report_path,
|
||||
title="API测试报告"
|
||||
)
|
||||
|
||||
content = report_path.read_text(encoding="utf-8")
|
||||
|
||||
assert "<!DOCTYPE html>" in content
|
||||
assert "<html" in content
|
||||
assert "<head>" in content
|
||||
assert "<body>" in content
|
||||
assert "API测试报告" in content
|
||||
assert "总用例数" in content
|
||||
assert "通过" in content
|
||||
assert "失败" in content
|
||||
assert "跳过" in content
|
||||
assert "通过率" in content
|
||||
assert "执行时长" in content
|
||||
assert "测试结果详情" in content
|
||||
assert "</html>" in content
|
||||
|
||||
|
||||
def test_generate_html_report_with_all_passed(report_manager, tmp_path):
|
||||
"""测试生成HTML报告(全部通过)"""
|
||||
test_cases = [
|
||||
TestCase(
|
||||
id="TC001",
|
||||
name="测试用例1",
|
||||
description="测试用例1",
|
||||
module="test",
|
||||
endpoint="/api/test1",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
enabled=True
|
||||
),
|
||||
TestCase(
|
||||
id="TC002",
|
||||
name="测试用例2",
|
||||
description="测试用例2",
|
||||
module="test",
|
||||
endpoint="/api/test2",
|
||||
method=HTTPMethod.POST,
|
||||
headers={},
|
||||
enabled=True
|
||||
)
|
||||
]
|
||||
|
||||
results = [
|
||||
TestResult(
|
||||
test_case=test_cases[0],
|
||||
passed=True,
|
||||
status_code=200,
|
||||
response_body={"message": "success"},
|
||||
response_headers={"Content-Type": "application/json"},
|
||||
performance=PerformanceMetrics(
|
||||
timestamp=datetime.now(),
|
||||
response_time=123,
|
||||
request_size=0,
|
||||
response_size=0
|
||||
)
|
||||
),
|
||||
TestResult(
|
||||
test_case=test_cases[1],
|
||||
passed=True,
|
||||
status_code=200,
|
||||
response_body={"message": "success"},
|
||||
response_headers={"Content-Type": "application/json"},
|
||||
performance=PerformanceMetrics(
|
||||
timestamp=datetime.now(),
|
||||
response_time=456,
|
||||
request_size=0,
|
||||
response_size=0
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
test_suite_result = TestSuiteResult(
|
||||
suite_name="Test Suite",
|
||||
total=2,
|
||||
passed=2,
|
||||
failed=0,
|
||||
skipped=0,
|
||||
results=results,
|
||||
start_time=datetime.now()
|
||||
)
|
||||
|
||||
report_path = tmp_path / "test_report.html"
|
||||
report_manager.generate_html_report(test_suite_result, report_path)
|
||||
|
||||
content = report_path.read_text(encoding="utf-8")
|
||||
assert "100.0%" in content or "100.0" in content
|
||||
|
||||
|
||||
def test_generate_html_report_with_all_failed(report_manager, tmp_path):
|
||||
"""测试生成HTML报告(全部失败)"""
|
||||
test_cases = [
|
||||
TestCase(
|
||||
id="TC001",
|
||||
name="测试用例1",
|
||||
description="测试用例1",
|
||||
module="test",
|
||||
endpoint="/api/test1",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
enabled=True
|
||||
),
|
||||
TestCase(
|
||||
id="TC002",
|
||||
name="测试用例2",
|
||||
description="测试用例2",
|
||||
module="test",
|
||||
endpoint="/api/test2",
|
||||
method=HTTPMethod.POST,
|
||||
headers={},
|
||||
enabled=True
|
||||
)
|
||||
]
|
||||
|
||||
results = [
|
||||
TestResult(
|
||||
test_case=test_cases[0],
|
||||
passed=False,
|
||||
status_code=500,
|
||||
response_body={"error": "error"},
|
||||
response_headers={"Content-Type": "application/json"},
|
||||
performance=PerformanceMetrics(
|
||||
timestamp=datetime.now(),
|
||||
response_time=123,
|
||||
request_size=0,
|
||||
response_size=0
|
||||
),
|
||||
error_message="错误1"
|
||||
),
|
||||
TestResult(
|
||||
test_case=test_cases[1],
|
||||
passed=False,
|
||||
status_code=404,
|
||||
response_body={"error": "not found"},
|
||||
response_headers={"Content-Type": "application/json"},
|
||||
performance=PerformanceMetrics(
|
||||
timestamp=datetime.now(),
|
||||
response_time=456,
|
||||
request_size=0,
|
||||
response_size=0
|
||||
),
|
||||
error_message="错误2"
|
||||
)
|
||||
]
|
||||
|
||||
test_suite_result = TestSuiteResult(
|
||||
suite_name="Test Suite",
|
||||
total=2,
|
||||
passed=0,
|
||||
failed=2,
|
||||
skipped=0,
|
||||
results=results,
|
||||
start_time=datetime.now()
|
||||
)
|
||||
|
||||
report_path = tmp_path / "test_report.html"
|
||||
report_manager.generate_html_report(test_suite_result, report_path)
|
||||
|
||||
content = report_path.read_text(encoding="utf-8")
|
||||
assert "0.0%" in content or "0.0" in content
|
||||
assert "错误1" in content
|
||||
assert "错误2" in content
|
||||
@@ -0,0 +1,297 @@
|
||||
"""测试数据管理器单元测试"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock
|
||||
from apitest.data.test_data_manager import TestDataManager
|
||||
from apitest.models.test_models import TestCase, HTTPMethod
|
||||
from apitest.models.exceptions import TestRunException
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_logger():
|
||||
"""创建模拟日志记录器"""
|
||||
return Mock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def data_manager(mock_logger):
|
||||
"""创建测试数据管理器实例"""
|
||||
return TestDataManager(logger=mock_logger)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_test_cases():
|
||||
"""创建示例测试用例"""
|
||||
return [
|
||||
TestCase(
|
||||
id="TC001",
|
||||
name="测试用例1",
|
||||
description="测试用例1",
|
||||
module="test",
|
||||
endpoint="/api/test1",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
enabled=True
|
||||
),
|
||||
TestCase(
|
||||
id="TC002",
|
||||
name="测试用例2",
|
||||
description="测试用例2",
|
||||
module="test",
|
||||
endpoint="/api/test2",
|
||||
method=HTTPMethod.POST,
|
||||
headers={},
|
||||
enabled=True
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_load_test_cases_from_json_success(data_manager, tmp_path):
|
||||
"""测试从JSON文件加载测试用例(成功)"""
|
||||
import json
|
||||
|
||||
test_data = [
|
||||
{
|
||||
"id": "TC001",
|
||||
"name": "测试用例1",
|
||||
"description": "测试用例1",
|
||||
"module": "test",
|
||||
"endpoint": "/api/test1",
|
||||
"method": "GET",
|
||||
"headers": {},
|
||||
"enabled": True
|
||||
},
|
||||
{
|
||||
"id": "TC002",
|
||||
"name": "测试用例2",
|
||||
"description": "测试用例2",
|
||||
"module": "test",
|
||||
"endpoint": "/api/test2",
|
||||
"method": "POST",
|
||||
"headers": {},
|
||||
"enabled": True
|
||||
}
|
||||
]
|
||||
|
||||
test_file = tmp_path / "test_cases.json"
|
||||
with open(test_file, "w", encoding="utf-8") as f:
|
||||
json.dump(test_data, f)
|
||||
|
||||
test_cases = data_manager.load_test_cases_from_json(test_file)
|
||||
|
||||
assert len(test_cases) == 2
|
||||
assert test_cases[0].id == "TC001"
|
||||
assert test_cases[1].id == "TC002"
|
||||
assert test_cases[0].method == HTTPMethod.GET
|
||||
assert test_cases[1].method == HTTPMethod.POST
|
||||
|
||||
|
||||
def test_load_test_cases_from_json_file_not_found(data_manager, tmp_path):
|
||||
"""测试从JSON文件加载测试用例(文件不存在)"""
|
||||
test_file = tmp_path / "nonexistent.json"
|
||||
|
||||
with pytest.raises(TestRunException) as exc_info:
|
||||
data_manager.load_test_cases_from_json(test_file)
|
||||
|
||||
assert "测试用例文件不存在" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_load_test_cases_from_json_invalid_json(data_manager, tmp_path):
|
||||
"""测试从JSON文件加载测试用例(无效JSON)"""
|
||||
test_file = tmp_path / "invalid.json"
|
||||
with open(test_file, "w", encoding="utf-8") as f:
|
||||
f.write("{ invalid json }")
|
||||
|
||||
with pytest.raises(TestRunException) as exc_info:
|
||||
data_manager.load_test_cases_from_json(test_file)
|
||||
|
||||
assert "JSON文件解析失败" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_load_test_cases_from_json_invalid_method(data_manager, tmp_path):
|
||||
"""测试从JSON文件加载测试用例(无效HTTP方法)"""
|
||||
import json
|
||||
|
||||
test_data = [
|
||||
{
|
||||
"id": "TC001",
|
||||
"name": "测试用例1",
|
||||
"description": "测试用例1",
|
||||
"module": "test",
|
||||
"endpoint": "/api/test1",
|
||||
"method": "INVALID",
|
||||
"headers": {},
|
||||
"enabled": True
|
||||
}
|
||||
]
|
||||
|
||||
test_file = tmp_path / "test_cases.json"
|
||||
with open(test_file, "w", encoding="utf-8") as f:
|
||||
json.dump(test_data, f)
|
||||
|
||||
test_cases = data_manager.load_test_cases_from_json(test_file)
|
||||
|
||||
assert len(test_cases) == 1
|
||||
assert test_cases[0].method == HTTPMethod.GET
|
||||
|
||||
|
||||
def test_load_test_data_from_csv_success(data_manager, tmp_path):
|
||||
"""测试从CSV文件加载测试数据(成功)"""
|
||||
test_file = tmp_path / "test_data.csv"
|
||||
with open(test_file, "w", encoding="utf-8", newline="") as f:
|
||||
f.write("param1,param2,param3\n")
|
||||
f.write("value1,value2,value3\n")
|
||||
f.write("value4,value5,value6\n")
|
||||
|
||||
test_data = data_manager.load_test_data_from_csv(test_file)
|
||||
|
||||
assert len(test_data) == 2
|
||||
assert test_data[0] == {"param1": "value1", "param2": "value2", "param3": "value3"}
|
||||
assert test_data[1] == {"param1": "value4", "param2": "value5", "param3": "value6"}
|
||||
|
||||
|
||||
def test_load_test_data_from_csv_file_not_found(data_manager, tmp_path):
|
||||
"""测试从CSV文件加载测试数据(文件不存在)"""
|
||||
test_file = tmp_path / "nonexistent.csv"
|
||||
|
||||
with pytest.raises(TestRunException) as exc_info:
|
||||
data_manager.load_test_data_from_csv(test_file)
|
||||
|
||||
assert "测试数据文件不存在" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_parameterize_test_case_success(data_manager, sample_test_cases):
|
||||
"""测试参数化测试用例(成功)"""
|
||||
test_data = [
|
||||
{"params": {"id": "1"}, "body": {"name": "test1"}},
|
||||
{"params": {"id": "2"}, "body": {"name": "test2"}}
|
||||
]
|
||||
|
||||
parameterized_cases = data_manager.parameterize_test_case(
|
||||
sample_test_cases[0],
|
||||
test_data
|
||||
)
|
||||
|
||||
assert len(parameterized_cases) == 2
|
||||
assert parameterized_cases[0].id == "TC001_1"
|
||||
assert parameterized_cases[0].name == "测试用例1 (数据集 1)"
|
||||
assert parameterized_cases[0].params == {"id": "1"}
|
||||
assert parameterized_cases[0].body == {"name": "test1"}
|
||||
assert parameterized_cases[1].id == "TC001_2"
|
||||
assert parameterized_cases[1].name == "测试用例1 (数据集 2)"
|
||||
assert parameterized_cases[1].params == {"id": "2"}
|
||||
assert parameterized_cases[1].body == {"name": "test2"}
|
||||
|
||||
|
||||
def test_parameterize_test_case_empty_data(data_manager, sample_test_cases):
|
||||
"""测试参数化测试用例(空数据)"""
|
||||
test_data = []
|
||||
|
||||
parameterized_cases = data_manager.parameterize_test_case(
|
||||
sample_test_cases[0],
|
||||
test_data
|
||||
)
|
||||
|
||||
assert len(parameterized_cases) == 0
|
||||
|
||||
|
||||
def test_save_test_cases_to_json_success(data_manager, sample_test_cases, tmp_path):
|
||||
"""测试保存测试用例到JSON文件(成功)"""
|
||||
output_file = tmp_path / "output.json"
|
||||
|
||||
data_manager.save_test_cases_to_json(sample_test_cases, output_file)
|
||||
|
||||
assert output_file.exists()
|
||||
|
||||
import json
|
||||
with open(output_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert len(data) == 2
|
||||
assert data[0]["id"] == "TC001"
|
||||
assert data[1]["id"] == "TC002"
|
||||
|
||||
|
||||
def test_save_test_cases_to_json_create_directory(data_manager, sample_test_cases, tmp_path):
|
||||
"""测试保存测试用例到JSON文件(创建目录)"""
|
||||
output_file = tmp_path / "nested" / "dir" / "output.json"
|
||||
|
||||
data_manager.save_test_cases_to_json(sample_test_cases, output_file)
|
||||
|
||||
assert output_file.exists()
|
||||
assert output_file.parent.exists()
|
||||
|
||||
|
||||
def test_save_test_data_to_csv_success(data_manager, tmp_path):
|
||||
"""测试保存测试数据到CSV文件(成功)"""
|
||||
test_data = [
|
||||
{"param1": "value1", "param2": "value2"},
|
||||
{"param1": "value3", "param2": "value4"}
|
||||
]
|
||||
|
||||
output_file = tmp_path / "output.csv"
|
||||
|
||||
data_manager.save_test_data_to_csv(test_data, output_file)
|
||||
|
||||
assert output_file.exists()
|
||||
|
||||
import csv
|
||||
with open(output_file, "r", encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
rows = list(reader)
|
||||
|
||||
assert len(rows) == 2
|
||||
assert rows[0]["param1"] == "value1"
|
||||
assert rows[1]["param1"] == "value3"
|
||||
|
||||
|
||||
def test_save_test_data_to_csv_with_fieldnames(data_manager, tmp_path):
|
||||
"""测试保存测试数据到CSV文件(指定字段名)"""
|
||||
test_data = [
|
||||
{"param1": "value1", "param2": "value2", "param3": "value3"},
|
||||
{"param1": "value4", "param2": "value5", "param3": "value6"}
|
||||
]
|
||||
|
||||
output_file = tmp_path / "output.csv"
|
||||
|
||||
data_manager.save_test_data_to_csv(
|
||||
test_data,
|
||||
output_file,
|
||||
fieldnames=["param1", "param2"]
|
||||
)
|
||||
|
||||
assert output_file.exists()
|
||||
|
||||
import csv
|
||||
with open(output_file, "r", encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
rows = list(reader)
|
||||
fieldnames = reader.fieldnames
|
||||
|
||||
assert len(rows) == 2
|
||||
assert fieldnames == ["param1", "param2"]
|
||||
|
||||
|
||||
def test_save_test_data_to_csv_empty_data(data_manager, tmp_path):
|
||||
"""测试保存测试数据到CSV文件(空数据)"""
|
||||
test_data = []
|
||||
|
||||
output_file = tmp_path / "output.csv"
|
||||
|
||||
with pytest.raises(TestRunException) as exc_info:
|
||||
data_manager.save_test_data_to_csv(test_data, output_file)
|
||||
|
||||
assert "测试数据为空" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_save_test_data_to_csv_create_directory(data_manager, tmp_path):
|
||||
"""测试保存测试数据到CSV文件(创建目录)"""
|
||||
test_data = [{"param1": "value1"}]
|
||||
|
||||
output_file = tmp_path / "nested" / "dir" / "output.csv"
|
||||
|
||||
data_manager.save_test_data_to_csv(test_data, output_file)
|
||||
|
||||
assert output_file.exists()
|
||||
assert output_file.parent.exists()
|
||||
@@ -0,0 +1,505 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from datetime import datetime
|
||||
from apitest.models.test_models import (
|
||||
TestCase, TestResult, TestSuiteResult, HTTPMethod, PerformanceMetrics
|
||||
)
|
||||
from apitest.core.test_engine import TestEngine
|
||||
from apitest.core.validation_engine import ValidationEngine
|
||||
from apitest.models.exceptions import TestRunException
|
||||
|
||||
|
||||
class TestTestEngine:
|
||||
"""测试TestEngine测试引擎"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_api_client(self):
|
||||
"""模拟API客户端"""
|
||||
api_client = MagicMock()
|
||||
api_client.request.return_value = {
|
||||
"status_code": 200,
|
||||
"response_body": {"message": "success"},
|
||||
"response_headers": {"Content-Type": "application/json"},
|
||||
"performance": PerformanceMetrics(
|
||||
timestamp=datetime.now(),
|
||||
response_time=1000,
|
||||
request_size=100,
|
||||
response_size=200
|
||||
)
|
||||
}
|
||||
return api_client
|
||||
|
||||
@pytest.fixture
|
||||
def mock_auth_manager(self):
|
||||
"""模拟认证管理器"""
|
||||
auth_manager = MagicMock()
|
||||
auth_manager.get_auth_headers.return_value = {"Authorization": "Bearer token"}
|
||||
return auth_manager
|
||||
|
||||
@pytest.fixture
|
||||
def mock_validation_engine(self):
|
||||
"""模拟验证引擎"""
|
||||
validation_engine = MagicMock()
|
||||
validation_engine.validate_response.return_value = (True, "")
|
||||
return validation_engine
|
||||
|
||||
@pytest.fixture
|
||||
def test_engine(self, mock_api_client, mock_auth_manager, mock_validation_engine):
|
||||
"""创建测试引擎实例"""
|
||||
return TestEngine(
|
||||
api_client=mock_api_client,
|
||||
auth_manager=mock_auth_manager,
|
||||
validation_engine=mock_validation_engine
|
||||
)
|
||||
|
||||
def test_test_engine_initialization(self, test_engine):
|
||||
"""测试测试引擎初始化"""
|
||||
assert test_engine.api_client is not None
|
||||
assert test_engine.auth_manager is not None
|
||||
assert test_engine.validation_engine is not None
|
||||
assert test_engine._context == {}
|
||||
|
||||
def test_set_context(self, test_engine):
|
||||
"""测试设置上下文变量"""
|
||||
test_engine.set_context("user_id", "12345")
|
||||
assert test_engine.get_context("user_id") == "12345"
|
||||
|
||||
def test_get_context_with_default(self, test_engine):
|
||||
"""测试获取上下文变量(带默认值)"""
|
||||
assert test_engine.get_context("nonexistent", "default") == "default"
|
||||
|
||||
def test_topological_sort_no_dependencies(self, test_engine):
|
||||
"""测试拓扑排序(无依赖)"""
|
||||
test_cases = [
|
||||
TestCase(
|
||||
id="TC001",
|
||||
name="测试1",
|
||||
description="测试用例1",
|
||||
module="test",
|
||||
endpoint="/api/test1",
|
||||
method=HTTPMethod.GET,
|
||||
headers={}
|
||||
),
|
||||
TestCase(
|
||||
id="TC002",
|
||||
name="测试2",
|
||||
description="测试用例2",
|
||||
module="test",
|
||||
endpoint="/api/test2",
|
||||
method=HTTPMethod.GET,
|
||||
headers={}
|
||||
)
|
||||
]
|
||||
|
||||
sorted_cases = test_engine._topological_sort(test_cases)
|
||||
assert len(sorted_cases) == 2
|
||||
|
||||
def test_topological_sort_with_dependencies(self, test_engine):
|
||||
"""测试拓扑排序(有依赖)"""
|
||||
test_cases = [
|
||||
TestCase(
|
||||
id="TC001",
|
||||
name="测试1",
|
||||
description="测试用例1",
|
||||
module="test",
|
||||
endpoint="/api/test1",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
dependencies=[]
|
||||
),
|
||||
TestCase(
|
||||
id="TC002",
|
||||
name="测试2",
|
||||
description="测试用例2",
|
||||
module="test",
|
||||
endpoint="/api/test2",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
dependencies=["TC001"]
|
||||
),
|
||||
TestCase(
|
||||
id="TC003",
|
||||
name="测试3",
|
||||
description="测试用例3",
|
||||
module="test",
|
||||
endpoint="/api/test3",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
dependencies=["TC002"]
|
||||
)
|
||||
]
|
||||
|
||||
sorted_cases = test_engine._topological_sort(test_cases)
|
||||
assert len(sorted_cases) == 3
|
||||
assert sorted_cases[0].id == "TC001"
|
||||
assert sorted_cases[1].id == "TC002"
|
||||
assert sorted_cases[2].id == "TC003"
|
||||
|
||||
def test_topological_sort_circular_dependency(self, test_engine):
|
||||
"""测试拓扑排序(循环依赖)"""
|
||||
test_cases = [
|
||||
TestCase(
|
||||
id="TC001",
|
||||
name="测试1",
|
||||
description="测试用例1",
|
||||
module="test",
|
||||
endpoint="/api/test1",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
dependencies=["TC002"]
|
||||
),
|
||||
TestCase(
|
||||
id="TC002",
|
||||
name="测试2",
|
||||
description="测试用例2",
|
||||
module="test",
|
||||
endpoint="/api/test2",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
dependencies=["TC001"]
|
||||
)
|
||||
]
|
||||
|
||||
with pytest.raises(TestRunException, match="存在循环依赖"):
|
||||
test_engine._topological_sort(test_cases)
|
||||
|
||||
def test_resolve_context_variables_string(self, test_engine):
|
||||
"""测试解析上下文变量(字符串)"""
|
||||
test_engine.set_context("user_id", "12345")
|
||||
result = test_engine._resolve_context_variables("${user_id}")
|
||||
assert result == "12345"
|
||||
|
||||
def test_resolve_context_variables_dict(self, test_engine):
|
||||
"""测试解析上下文变量(字典)"""
|
||||
test_engine.set_context("user_id", "12345")
|
||||
data = {"user_id": "${user_id}", "name": "test"}
|
||||
result = test_engine._resolve_context_variables(data)
|
||||
assert result == {"user_id": "12345", "name": "test"}
|
||||
|
||||
def test_resolve_context_variables_list(self, test_engine):
|
||||
"""测试解析上下文变量(列表)"""
|
||||
test_engine.set_context("user_id", "12345")
|
||||
data = ["${user_id}", "test"]
|
||||
result = test_engine._resolve_context_variables(data)
|
||||
assert result == ["12345", "test"]
|
||||
|
||||
def test_execute_test_case_success(self, test_engine):
|
||||
"""测试执行测试用例(成功)"""
|
||||
test_case = TestCase(
|
||||
id="TC001",
|
||||
name="测试用例",
|
||||
description="测试用例描述",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "status_code", "value": 200}]
|
||||
)
|
||||
|
||||
result = test_engine._execute_test_case(test_case)
|
||||
|
||||
assert isinstance(result, TestResult)
|
||||
assert result.passed == True
|
||||
assert result.status_code == 200
|
||||
assert result.test_case == test_case
|
||||
|
||||
def test_execute_test_case_with_auth(self, test_engine):
|
||||
"""测试执行测试用例(带认证)"""
|
||||
test_case = TestCase(
|
||||
id="TC001",
|
||||
name="测试用例",
|
||||
description="测试用例描述",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
auth_required=True
|
||||
)
|
||||
|
||||
result = test_engine._execute_test_case(test_case)
|
||||
|
||||
assert result.passed == True
|
||||
test_engine.auth_manager.get_auth_headers.assert_called_once()
|
||||
|
||||
def test_execute_test_case_failure(self, test_engine, mock_validation_engine):
|
||||
"""测试执行测试用例(失败)"""
|
||||
mock_validation_engine.validate_response.return_value = (False, "验证失败")
|
||||
|
||||
test_case = TestCase(
|
||||
id="TC001",
|
||||
name="测试用例",
|
||||
description="测试用例描述",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "status_code", "value": 200}]
|
||||
)
|
||||
|
||||
result = test_engine._execute_test_case(test_case)
|
||||
|
||||
assert result.passed == False
|
||||
assert result.error_message == "验证失败"
|
||||
|
||||
def test_execute_test_case_with_setup(self, test_engine):
|
||||
"""测试执行测试用例(带前置操作)"""
|
||||
test_case = TestCase(
|
||||
id="TC001",
|
||||
name="测试用例",
|
||||
description="测试用例描述",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
setup={"type": "set_context", "key": "test_key", "value": "test_value"}
|
||||
)
|
||||
|
||||
result = test_engine._execute_test_case(test_case)
|
||||
|
||||
assert test_engine.get_context("test_key") == "test_value"
|
||||
|
||||
def test_execute_test_case_with_teardown(self, test_engine):
|
||||
"""测试执行测试用例(带后置操作)"""
|
||||
test_engine.set_context("test_key", "test_value")
|
||||
|
||||
test_case = TestCase(
|
||||
id="TC001",
|
||||
name="测试用例",
|
||||
description="测试用例描述",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
teardown={"type": "clear_context", "key": "test_key"}
|
||||
)
|
||||
|
||||
result = test_engine._execute_test_case(test_case)
|
||||
|
||||
assert test_engine.get_context("test_key") is None
|
||||
|
||||
def test_execute_test_suite(self, test_engine):
|
||||
"""测试执行测试套件"""
|
||||
test_cases = [
|
||||
TestCase(
|
||||
id="TC001",
|
||||
name="测试1",
|
||||
description="测试用例1",
|
||||
module="test",
|
||||
endpoint="/api/test1",
|
||||
method=HTTPMethod.GET,
|
||||
headers={}
|
||||
),
|
||||
TestCase(
|
||||
id="TC002",
|
||||
name="测试2",
|
||||
description="测试用例2",
|
||||
module="test",
|
||||
endpoint="/api/test2",
|
||||
method=HTTPMethod.GET,
|
||||
headers={}
|
||||
)
|
||||
]
|
||||
|
||||
result = test_engine.execute_test_suite(test_cases)
|
||||
|
||||
assert isinstance(result, TestSuiteResult)
|
||||
assert len(result.results) == 2
|
||||
assert result.passed == 2
|
||||
assert result.failed == 0
|
||||
|
||||
def test_execute_test_suite_with_dependencies(self, test_engine):
|
||||
"""测试执行测试套件(有依赖)"""
|
||||
test_cases = [
|
||||
TestCase(
|
||||
id="TC001",
|
||||
name="测试1",
|
||||
description="测试用例1",
|
||||
module="test",
|
||||
endpoint="/api/test1",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
dependencies=[]
|
||||
),
|
||||
TestCase(
|
||||
id="TC002",
|
||||
name="测试2",
|
||||
description="测试用例2",
|
||||
module="test",
|
||||
endpoint="/api/test2",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
dependencies=["TC001"]
|
||||
)
|
||||
]
|
||||
|
||||
result = test_engine.execute_test_suite(test_cases)
|
||||
|
||||
assert len(result.results) == 2
|
||||
assert result.passed == 2
|
||||
|
||||
def test_execute_test_suite_stop_on_failure(self, test_engine, mock_validation_engine):
|
||||
"""测试执行测试套件(失败时停止)"""
|
||||
mock_validation_engine.validate_response.return_value = (False, "验证失败")
|
||||
|
||||
test_cases = [
|
||||
TestCase(
|
||||
id="TC001",
|
||||
name="测试1",
|
||||
description="测试用例1",
|
||||
module="test",
|
||||
endpoint="/api/test1",
|
||||
method=HTTPMethod.GET,
|
||||
headers={}
|
||||
),
|
||||
TestCase(
|
||||
id="TC002",
|
||||
name="测试2",
|
||||
description="测试用例2",
|
||||
module="test",
|
||||
endpoint="/api/test2",
|
||||
method=HTTPMethod.GET,
|
||||
headers={}
|
||||
)
|
||||
]
|
||||
|
||||
result = test_engine.execute_test_suite(test_cases, stop_on_failure=True)
|
||||
|
||||
assert len(result.results) == 1
|
||||
assert result.passed == 0
|
||||
assert result.failed == 1
|
||||
|
||||
def test_execute_test_suite_skip_disabled(self, test_engine):
|
||||
"""测试执行测试套件(跳过已禁用的用例)"""
|
||||
test_cases = [
|
||||
TestCase(
|
||||
id="TC001",
|
||||
name="测试1",
|
||||
description="测试用例1",
|
||||
module="test",
|
||||
endpoint="/api/test1",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
enabled=False
|
||||
),
|
||||
TestCase(
|
||||
id="TC002",
|
||||
name="测试2",
|
||||
description="测试用例2",
|
||||
module="test",
|
||||
endpoint="/api/test2",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
enabled=True
|
||||
)
|
||||
]
|
||||
|
||||
result = test_engine.execute_test_suite(test_cases)
|
||||
|
||||
assert len(result.results) == 1
|
||||
assert result.skipped == 1
|
||||
|
||||
def test_execute_test_cases_by_filter_module(self, test_engine):
|
||||
"""测试按模块过滤执行测试用例"""
|
||||
test_cases = [
|
||||
TestCase(
|
||||
id="TC001",
|
||||
name="测试1",
|
||||
description="测试用例1",
|
||||
module="module1",
|
||||
endpoint="/api/test1",
|
||||
method=HTTPMethod.GET,
|
||||
headers={}
|
||||
),
|
||||
TestCase(
|
||||
id="TC002",
|
||||
name="测试2",
|
||||
description="测试用例2",
|
||||
module="module2",
|
||||
endpoint="/api/test2",
|
||||
method=HTTPMethod.GET,
|
||||
headers={}
|
||||
)
|
||||
]
|
||||
|
||||
result = test_engine.execute_test_cases_by_filter(test_cases, module_filter="module1")
|
||||
|
||||
assert len(result.results) == 1
|
||||
assert result.results[0].test_case.module == "module1"
|
||||
|
||||
def test_execute_test_cases_by_filter_tag(self, test_engine):
|
||||
"""测试按标签过滤执行测试用例"""
|
||||
test_cases = [
|
||||
TestCase(
|
||||
id="TC001",
|
||||
name="测试1",
|
||||
description="测试用例1",
|
||||
module="test",
|
||||
endpoint="/api/test1",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
tags=["smoke", "regression"]
|
||||
),
|
||||
TestCase(
|
||||
id="TC002",
|
||||
name="测试2",
|
||||
description="测试用例2",
|
||||
module="test",
|
||||
endpoint="/api/test2",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
tags=["regression"]
|
||||
)
|
||||
]
|
||||
|
||||
result = test_engine.execute_test_cases_by_filter(test_cases, tag_filter=["smoke"])
|
||||
|
||||
assert len(result.results) == 1
|
||||
assert "smoke" in result.results[0].test_case.tags
|
||||
|
||||
def test_execute_test_cases_by_filter_priority(self, test_engine):
|
||||
"""测试按优先级过滤执行测试用例"""
|
||||
test_cases = [
|
||||
TestCase(
|
||||
id="TC001",
|
||||
name="测试1",
|
||||
description="测试用例1",
|
||||
module="test",
|
||||
endpoint="/api/test1",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
priority=1
|
||||
),
|
||||
TestCase(
|
||||
id="TC002",
|
||||
name="测试2",
|
||||
description="测试用例2",
|
||||
module="test",
|
||||
endpoint="/api/test2",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
priority=2
|
||||
)
|
||||
]
|
||||
|
||||
result = test_engine.execute_test_cases_by_filter(test_cases, priority_filter=1)
|
||||
|
||||
assert len(result.results) == 1
|
||||
assert result.results[0].test_case.priority == 1
|
||||
|
||||
def test_extract_response_data(self, test_engine):
|
||||
"""测试提取响应数据到上下文"""
|
||||
test_case = TestCase(
|
||||
id="TC001",
|
||||
name="测试用例",
|
||||
description="测试用例描述",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "extract", "field": "user_id", "var_name": "extracted_id"}]
|
||||
)
|
||||
|
||||
response_body = {"user_id": "12345", "name": "test"}
|
||||
test_engine._extract_response_data(test_case, response_body)
|
||||
|
||||
assert test_engine.get_context("extracted_id") == "12345"
|
||||
@@ -0,0 +1,373 @@
|
||||
"""测试编排器单元测试"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, MagicMock, patch
|
||||
from apitest.orchestrator.test_orchestrator import TestOrchestrator
|
||||
from apitest.models.test_models import TestCase, TestSuiteResult, HTTPMethod
|
||||
from apitest.models.exceptions import TestRunException
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_manager():
|
||||
"""创建模拟配置管理器"""
|
||||
config_manager = Mock()
|
||||
config_manager.get_base_url.return_value = "http://localhost:8080"
|
||||
config_manager.get_timeout.return_value = 30
|
||||
config_manager.get_log_level.return_value = "INFO"
|
||||
config_manager.get_log_format.return_value = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
return config_manager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_logger_manager():
|
||||
"""创建模拟日志管理器"""
|
||||
logger_manager = Mock()
|
||||
logger = Mock()
|
||||
logger_manager.get_logger.return_value = logger
|
||||
return logger_manager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_orchestrator(mock_config_manager, mock_logger_manager):
|
||||
"""创建测试编排器实例"""
|
||||
return TestOrchestrator(
|
||||
config_manager=mock_config_manager,
|
||||
logger_manager=mock_logger_manager
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_test_cases():
|
||||
"""创建示例测试用例"""
|
||||
return [
|
||||
TestCase(
|
||||
id="TC001",
|
||||
name="测试用例1",
|
||||
description="测试用例1",
|
||||
module="test",
|
||||
endpoint="/api/test1",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
enabled=True
|
||||
),
|
||||
TestCase(
|
||||
id="TC002",
|
||||
name="测试用例2",
|
||||
description="测试用例2",
|
||||
module="test",
|
||||
endpoint="/api/test2",
|
||||
method=HTTPMethod.POST,
|
||||
headers={},
|
||||
enabled=True
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_init_test_orchestrator(mock_config_manager, mock_logger_manager):
|
||||
"""测试初始化测试编排器"""
|
||||
orchestrator = TestOrchestrator(
|
||||
config_manager=mock_config_manager,
|
||||
logger_manager=mock_logger_manager
|
||||
)
|
||||
|
||||
assert orchestrator.config_manager == mock_config_manager
|
||||
assert orchestrator.logger_manager == mock_logger_manager
|
||||
assert orchestrator.api_client is not None
|
||||
assert orchestrator.auth_manager is not None
|
||||
assert orchestrator.validation_engine is not None
|
||||
assert orchestrator.test_engine is not None
|
||||
assert orchestrator.report_manager is not None
|
||||
assert orchestrator.logger is not None
|
||||
|
||||
|
||||
def test_init_test_orchestrator_without_managers():
|
||||
"""测试初始化测试编排器(不提供管理器)"""
|
||||
mock_logger = Mock()
|
||||
orchestrator = TestOrchestrator(logger=mock_logger)
|
||||
|
||||
assert orchestrator.config_manager is not None
|
||||
assert orchestrator.logger_manager is not None
|
||||
assert orchestrator.api_client is not None
|
||||
assert orchestrator.auth_manager is not None
|
||||
assert orchestrator.validation_engine is not None
|
||||
assert orchestrator.test_engine is not None
|
||||
assert orchestrator.report_manager is not None
|
||||
assert orchestrator.logger is not None
|
||||
|
||||
|
||||
def test_load_test_cases_success(test_orchestrator, tmp_path):
|
||||
"""测试加载测试用例(成功)"""
|
||||
import json
|
||||
|
||||
test_data = [
|
||||
{
|
||||
"id": "TC001",
|
||||
"name": "测试用例1",
|
||||
"description": "测试用例1",
|
||||
"module": "test",
|
||||
"endpoint": "/api/test1",
|
||||
"method": "GET",
|
||||
"headers": {},
|
||||
"enabled": True
|
||||
},
|
||||
{
|
||||
"id": "TC002",
|
||||
"name": "测试用例2",
|
||||
"description": "测试用例2",
|
||||
"module": "test",
|
||||
"endpoint": "/api/test2",
|
||||
"method": "POST",
|
||||
"headers": {},
|
||||
"enabled": True
|
||||
}
|
||||
]
|
||||
|
||||
test_file = tmp_path / "test_cases.json"
|
||||
with open(test_file, "w", encoding="utf-8") as f:
|
||||
json.dump(test_data, f)
|
||||
|
||||
test_cases = test_orchestrator.load_test_cases(test_file)
|
||||
|
||||
assert len(test_cases) == 2
|
||||
assert test_cases[0].id == "TC001"
|
||||
assert test_cases[1].id == "TC002"
|
||||
|
||||
|
||||
def test_load_test_cases_file_not_found(test_orchestrator, tmp_path):
|
||||
"""测试加载测试用例(文件不存在)"""
|
||||
test_file = tmp_path / "nonexistent.json"
|
||||
|
||||
with pytest.raises(TestRunException) as exc_info:
|
||||
test_orchestrator.load_test_cases(test_file)
|
||||
|
||||
assert "测试用例文件不存在" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_load_test_cases_invalid_json(test_orchestrator, tmp_path):
|
||||
"""测试加载测试用例(无效JSON)"""
|
||||
test_file = tmp_path / "invalid.json"
|
||||
with open(test_file, "w", encoding="utf-8") as f:
|
||||
f.write("invalid json")
|
||||
|
||||
with pytest.raises(TestRunException) as exc_info:
|
||||
test_orchestrator.load_test_cases(test_file)
|
||||
|
||||
assert "加载测试用例失败" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_load_test_cases_with_all_fields(test_orchestrator, tmp_path):
|
||||
"""测试加载测试用例(包含所有字段)"""
|
||||
import json
|
||||
|
||||
test_data = [
|
||||
{
|
||||
"id": "TC001",
|
||||
"name": "测试用例1",
|
||||
"description": "测试用例1",
|
||||
"module": "test",
|
||||
"endpoint": "/api/test1",
|
||||
"method": "GET",
|
||||
"headers": {"Content-Type": "application/json"},
|
||||
"params": {"param1": "value1"},
|
||||
"body": {"key": "value"},
|
||||
"dependencies": ["TC002"],
|
||||
"tags": ["tag1", "tag2"],
|
||||
"priority": 1,
|
||||
"enabled": True,
|
||||
"timeout": 10,
|
||||
"validations": [{"type": "status_code", "value": 200}],
|
||||
"extract_config": [{"type": "extract", "field": "id", "var_name": "user_id"}]
|
||||
}
|
||||
]
|
||||
|
||||
test_file = tmp_path / "test_cases.json"
|
||||
with open(test_file, "w", encoding="utf-8") as f:
|
||||
json.dump(test_data, f)
|
||||
|
||||
test_cases = test_orchestrator.load_test_cases(test_file)
|
||||
|
||||
assert len(test_cases) == 1
|
||||
assert test_cases[0].id == "TC001"
|
||||
assert test_cases[0].method == HTTPMethod.GET
|
||||
assert test_cases[0].headers == {"Content-Type": "application/json"}
|
||||
assert test_cases[0].params == {"param1": "value1"}
|
||||
assert test_cases[0].body == {"key": "value"}
|
||||
assert test_cases[0].dependencies == ["TC002"]
|
||||
assert test_cases[0].tags == ["tag1", "tag2"]
|
||||
assert test_cases[0].priority == 1
|
||||
assert test_cases[0].enabled == True
|
||||
assert test_cases[0].timeout == 10
|
||||
assert len(test_cases[0].validations) == 1
|
||||
|
||||
|
||||
def test_run_test_suite_success(test_orchestrator, sample_test_cases, tmp_path):
|
||||
"""测试运行测试套件(成功)"""
|
||||
report_path = tmp_path / "test_report.html"
|
||||
|
||||
with patch.object(test_orchestrator.test_engine, 'execute_test_suite') as mock_execute:
|
||||
mock_result = Mock(spec=TestSuiteResult)
|
||||
mock_result.suite_name = "Test Suite"
|
||||
mock_result.total = 2
|
||||
mock_result.passed = 2
|
||||
mock_result.failed = 0
|
||||
mock_result.skipped = 0
|
||||
mock_result.pass_rate = 100.0
|
||||
mock_result.duration = 1.0
|
||||
mock_execute.return_value = mock_result
|
||||
|
||||
result = test_orchestrator.run_test_suite(
|
||||
sample_test_cases,
|
||||
generate_report=False
|
||||
)
|
||||
|
||||
assert result == mock_result
|
||||
mock_execute.assert_called_once_with(sample_test_cases, stop_on_failure=False)
|
||||
|
||||
|
||||
def test_run_test_suite_with_report(test_orchestrator, sample_test_cases, tmp_path):
|
||||
"""测试运行测试套件(生成报告)"""
|
||||
report_path = tmp_path / "test_report.html"
|
||||
|
||||
with patch.object(test_orchestrator.test_engine, 'execute_test_suite') as mock_execute, \
|
||||
patch.object(test_orchestrator.report_manager, 'generate_html_report') as mock_report:
|
||||
mock_result = Mock(spec=TestSuiteResult)
|
||||
mock_result.suite_name = "Test Suite"
|
||||
mock_result.total = 2
|
||||
mock_result.passed = 2
|
||||
mock_result.failed = 0
|
||||
mock_result.skipped = 0
|
||||
mock_result.pass_rate = 100.0
|
||||
mock_result.duration = 1.0
|
||||
mock_execute.return_value = mock_result
|
||||
|
||||
result = test_orchestrator.run_test_suite(
|
||||
sample_test_cases,
|
||||
generate_report=True,
|
||||
report_format="html",
|
||||
report_path=report_path
|
||||
)
|
||||
|
||||
assert result == mock_result
|
||||
mock_report.assert_called_once()
|
||||
|
||||
|
||||
def test_run_test_suite_stop_on_failure(test_orchestrator, sample_test_cases):
|
||||
"""测试运行测试套件(失败时停止)"""
|
||||
with patch.object(test_orchestrator.test_engine, 'execute_test_suite') as mock_execute:
|
||||
mock_result = Mock(spec=TestSuiteResult)
|
||||
mock_result.suite_name = "Test Suite"
|
||||
mock_result.total = 2
|
||||
mock_result.passed = 1
|
||||
mock_result.failed = 1
|
||||
mock_result.skipped = 0
|
||||
mock_result.pass_rate = 50.0
|
||||
mock_result.duration = 0.5
|
||||
mock_execute.return_value = mock_result
|
||||
|
||||
result = test_orchestrator.run_test_suite(
|
||||
sample_test_cases,
|
||||
stop_on_failure=True,
|
||||
generate_report=False
|
||||
)
|
||||
|
||||
assert result == mock_result
|
||||
mock_execute.assert_called_once_with(sample_test_cases, stop_on_failure=True)
|
||||
|
||||
|
||||
def test_run_test_suite_by_filter_module(test_orchestrator, sample_test_cases):
|
||||
"""测试按模块过滤运行测试套件"""
|
||||
with patch.object(test_orchestrator.test_engine, 'execute_test_cases_by_filter') as mock_execute:
|
||||
mock_result = Mock(spec=TestSuiteResult)
|
||||
mock_result.suite_name = "Test Suite"
|
||||
mock_result.total = 2
|
||||
mock_result.passed = 2
|
||||
mock_result.failed = 0
|
||||
mock_result.skipped = 0
|
||||
mock_result.pass_rate = 100.0
|
||||
mock_result.duration = 1.0
|
||||
mock_execute.return_value = mock_result
|
||||
|
||||
result = test_orchestrator.run_test_suite_by_filter(
|
||||
sample_test_cases,
|
||||
module_filter="test",
|
||||
generate_report=False
|
||||
)
|
||||
|
||||
assert result == mock_result
|
||||
mock_execute.assert_called_once_with(
|
||||
sample_test_cases,
|
||||
module_filter="test",
|
||||
tag_filter=None,
|
||||
priority_filter=None
|
||||
)
|
||||
|
||||
|
||||
def test_run_test_suite_by_filter_tag(test_orchestrator, sample_test_cases):
|
||||
"""测试按标签过滤运行测试套件"""
|
||||
with patch.object(test_orchestrator.test_engine, 'execute_test_cases_by_filter') as mock_execute:
|
||||
mock_result = Mock(spec=TestSuiteResult)
|
||||
mock_result.suite_name = "Test Suite"
|
||||
mock_result.total = 2
|
||||
mock_result.passed = 2
|
||||
mock_result.failed = 0
|
||||
mock_result.skipped = 0
|
||||
mock_result.pass_rate = 100.0
|
||||
mock_result.duration = 1.0
|
||||
mock_execute.return_value = mock_result
|
||||
|
||||
result = test_orchestrator.run_test_suite_by_filter(
|
||||
sample_test_cases,
|
||||
tag_filter=["smoke"],
|
||||
generate_report=False
|
||||
)
|
||||
|
||||
assert result == mock_result
|
||||
mock_execute.assert_called_once_with(
|
||||
sample_test_cases,
|
||||
module_filter=None,
|
||||
tag_filter=["smoke"],
|
||||
priority_filter=None
|
||||
)
|
||||
|
||||
|
||||
def test_run_test_suite_by_filter_priority(test_orchestrator, sample_test_cases):
|
||||
"""测试按优先级过滤运行测试套件"""
|
||||
with patch.object(test_orchestrator.test_engine, 'execute_test_cases_by_filter') as mock_execute:
|
||||
mock_result = Mock(spec=TestSuiteResult)
|
||||
mock_result.suite_name = "Test Suite"
|
||||
mock_result.total = 2
|
||||
mock_result.passed = 2
|
||||
mock_result.failed = 0
|
||||
mock_result.skipped = 0
|
||||
mock_result.pass_rate = 100.0
|
||||
mock_result.duration = 1.0
|
||||
mock_execute.return_value = mock_result
|
||||
|
||||
result = test_orchestrator.run_test_suite_by_filter(
|
||||
sample_test_cases,
|
||||
priority_filter=1,
|
||||
generate_report=False
|
||||
)
|
||||
|
||||
assert result == mock_result
|
||||
mock_execute.assert_called_once_with(
|
||||
sample_test_cases,
|
||||
module_filter=None,
|
||||
tag_filter=None,
|
||||
priority_filter=1
|
||||
)
|
||||
|
||||
|
||||
def test_set_base_url(test_orchestrator):
|
||||
"""测试设置基础URL"""
|
||||
test_orchestrator.set_base_url("http://new-api.example.com")
|
||||
|
||||
assert test_orchestrator.api_client.base_url == "http://new-api.example.com"
|
||||
|
||||
|
||||
def test_set_auth_token(test_orchestrator):
|
||||
"""测试设置认证令牌"""
|
||||
test_orchestrator.set_auth_token("test-token-123")
|
||||
|
||||
assert test_orchestrator.auth_manager.get_token() == "test-token-123"
|
||||
@@ -0,0 +1,480 @@
|
||||
import pytest
|
||||
from apitest.models.test_models import TestCase, HTTPMethod, PerformanceMetrics
|
||||
from apitest.core.validation_engine import ValidationEngine
|
||||
from apitest.models.exceptions import ValidationException
|
||||
|
||||
|
||||
class TestValidationEngine:
|
||||
"""测试ValidationEngine验证引擎"""
|
||||
|
||||
@pytest.fixture
|
||||
def validation_engine(self):
|
||||
"""创建验证引擎实例"""
|
||||
return ValidationEngine()
|
||||
|
||||
def test_validate_status_code_success(self, validation_engine):
|
||||
"""测试状态码验证成功"""
|
||||
test_case = TestCase(
|
||||
id="TC001",
|
||||
name="测试状态码",
|
||||
description="测试状态码验证",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "status_code", "value": 200}]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{"message": "success"},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == True
|
||||
assert error == ""
|
||||
|
||||
def test_validate_status_code_failure(self, validation_engine):
|
||||
"""测试状态码验证失败"""
|
||||
test_case = TestCase(
|
||||
id="TC001",
|
||||
name="测试状态码",
|
||||
description="测试状态码验证",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "status_code", "value": 200}]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
404,
|
||||
{"message": "not found"},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == False
|
||||
assert "状态码验证失败" in error
|
||||
|
||||
def test_validate_contains_success(self, validation_engine):
|
||||
"""测试包含验证成功"""
|
||||
test_case = TestCase(
|
||||
id="TC002",
|
||||
name="测试包含",
|
||||
description="测试包含验证",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "contains", "value": "success"}]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{"message": "operation success"},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == True
|
||||
assert error == ""
|
||||
|
||||
def test_validate_contains_failure(self, validation_engine):
|
||||
"""测试包含验证失败"""
|
||||
test_case = TestCase(
|
||||
id="TC002",
|
||||
name="测试包含",
|
||||
description="测试包含验证",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "contains", "value": "error"}]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{"message": "operation success"},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == False
|
||||
assert "包含验证失败" in error
|
||||
|
||||
def test_validate_contains_with_field(self, validation_engine):
|
||||
"""测试字段包含验证"""
|
||||
test_case = TestCase(
|
||||
id="TC003",
|
||||
name="测试字段包含",
|
||||
description="测试字段包含验证",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "contains", "field": "message", "value": "success"}]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{"message": "operation success"},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == True
|
||||
|
||||
def test_validate_equals_success(self, validation_engine):
|
||||
"""测试相等验证成功"""
|
||||
test_case = TestCase(
|
||||
id="TC004",
|
||||
name="测试相等",
|
||||
description="测试相等验证",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "equals", "field": "status", "value": "ok"}]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{"status": "ok"},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == True
|
||||
|
||||
def test_validate_equals_failure(self, validation_engine):
|
||||
"""测试相等验证失败"""
|
||||
test_case = TestCase(
|
||||
id="TC004",
|
||||
name="测试相等",
|
||||
description="测试相等验证",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "equals", "field": "status", "value": "ok"}]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{"status": "error"},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == False
|
||||
assert "相等验证失败" in error
|
||||
|
||||
def test_validate_json_path_success(self, validation_engine):
|
||||
"""测试JSON路径验证成功"""
|
||||
test_case = TestCase(
|
||||
id="TC005",
|
||||
name="测试JSON路径",
|
||||
description="测试JSON路径验证",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "json_path", "path": "data.user.name", "value": "John"}]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{"data": {"user": {"name": "John"}}},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == True
|
||||
|
||||
def test_validate_json_path_failure(self, validation_engine):
|
||||
"""测试JSON路径验证失败"""
|
||||
test_case = TestCase(
|
||||
id="TC005",
|
||||
name="测试JSON路径",
|
||||
description="测试JSON路径验证",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "json_path", "path": "data.user.name", "value": "Jane"}]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{"data": {"user": {"name": "John"}}},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == False
|
||||
assert "JSON路径验证失败" in error
|
||||
|
||||
def test_validate_regex_success(self, validation_engine):
|
||||
"""测试正则表达式验证成功"""
|
||||
test_case = TestCase(
|
||||
id="TC006",
|
||||
name="测试正则表达式",
|
||||
description="测试正则表达式验证",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "regex", "field": "email", "pattern": r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"}]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{"email": "test@example.com"},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == True
|
||||
|
||||
def test_validate_regex_failure(self, validation_engine):
|
||||
"""测试正则表达式验证失败"""
|
||||
test_case = TestCase(
|
||||
id="TC006",
|
||||
name="测试正则表达式",
|
||||
description="测试正则表达式验证",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "regex", "field": "email", "pattern": r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"}]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{"email": "invalid-email"},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == False
|
||||
assert "正则表达式验证失败" in error
|
||||
|
||||
def test_validate_header_success(self, validation_engine):
|
||||
"""测试响应头验证成功"""
|
||||
test_case = TestCase(
|
||||
id="TC007",
|
||||
name="测试响应头",
|
||||
description="测试响应头验证",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "header", "name": "Content-Type", "value": "application/json"}]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{},
|
||||
{"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
assert passed == True
|
||||
|
||||
def test_validate_header_not_found(self, validation_engine):
|
||||
"""测试响应头不存在"""
|
||||
test_case = TestCase(
|
||||
id="TC007",
|
||||
name="测试响应头",
|
||||
description="测试响应头验证",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "header", "name": "X-Custom-Header"}]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{},
|
||||
{"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
assert passed == False
|
||||
assert "响应头中未找到" in error
|
||||
|
||||
def test_validate_schema_success(self, validation_engine):
|
||||
"""测试结构验证成功"""
|
||||
test_case = TestCase(
|
||||
id="TC008",
|
||||
name="测试结构",
|
||||
description="测试结构验证",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "schema", "schema": {"name": "str", "age": "int"}}]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{"name": "John", "age": 30},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == True
|
||||
|
||||
def test_validate_schema_failure(self, validation_engine):
|
||||
"""测试结构验证失败"""
|
||||
test_case = TestCase(
|
||||
id="TC008",
|
||||
name="测试结构",
|
||||
description="测试结构验证",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "schema", "schema": {"name": "str", "age": "int"}}]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{"name": "John", "age": "thirty"},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == False
|
||||
assert "字段 age 类型错误" in error
|
||||
|
||||
def test_validate_performance_success(self, validation_engine):
|
||||
"""测试性能验证成功"""
|
||||
from datetime import datetime
|
||||
performance = PerformanceMetrics(
|
||||
timestamp=datetime.now(),
|
||||
response_time=1000,
|
||||
request_size=100,
|
||||
response_size=200
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_performance(performance, 5000)
|
||||
|
||||
assert passed == True
|
||||
assert error == ""
|
||||
|
||||
def test_validate_performance_failure(self, validation_engine):
|
||||
"""测试性能验证失败"""
|
||||
from datetime import datetime
|
||||
performance = PerformanceMetrics(
|
||||
timestamp=datetime.now(),
|
||||
response_time=6000,
|
||||
request_size=100,
|
||||
response_size=200
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_performance(performance, 5000)
|
||||
|
||||
assert passed == False
|
||||
assert "响应时间超过阈值" in error
|
||||
|
||||
def test_validate_multiple_validations(self, validation_engine):
|
||||
"""测试多个验证规则"""
|
||||
test_case = TestCase(
|
||||
id="TC009",
|
||||
name="测试多验证",
|
||||
description="测试多个验证规则",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[
|
||||
{"type": "status_code", "value": 200},
|
||||
{"type": "contains", "value": "success"},
|
||||
{"type": "equals", "field": "status", "value": "ok"}
|
||||
]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{"status": "ok", "message": "operation success"},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == True
|
||||
|
||||
def test_validate_multiple_validations_failure(self, validation_engine):
|
||||
"""测试多个验证规则(其中一个失败)"""
|
||||
test_case = TestCase(
|
||||
id="TC009",
|
||||
name="测试多验证",
|
||||
description="测试多个验证规则",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[
|
||||
{"type": "status_code", "value": 200},
|
||||
{"type": "contains", "value": "error"},
|
||||
{"type": "equals", "field": "status", "value": "ok"}
|
||||
]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{"status": "ok", "message": "operation success"},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == False
|
||||
assert "包含验证失败" in error
|
||||
|
||||
def test_validate_no_validations(self, validation_engine):
|
||||
"""测试无验证规则"""
|
||||
test_case = TestCase(
|
||||
id="TC010",
|
||||
name="测试无验证",
|
||||
description="测试无验证规则",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={}
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{"message": "success"},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == True
|
||||
assert error == ""
|
||||
|
||||
def test_validate_unsupported_type(self, validation_engine):
|
||||
"""测试不支持的验证类型"""
|
||||
test_case = TestCase(
|
||||
id="TC011",
|
||||
name="测试不支持的类型",
|
||||
description="测试不支持的验证类型",
|
||||
module="test",
|
||||
endpoint="/api/test",
|
||||
method=HTTPMethod.GET,
|
||||
headers={},
|
||||
validations=[{"type": "unsupported_type"}]
|
||||
)
|
||||
|
||||
passed, error = validation_engine.validate_response(
|
||||
test_case,
|
||||
200,
|
||||
{"message": "success"},
|
||||
{}
|
||||
)
|
||||
|
||||
assert passed == False
|
||||
assert "不支持的验证类型" in error
|
||||
Reference in New Issue
Block a user