feat(admin): 添加用户管理相关文件

添加用户管理视图、API和状态管理文件
This commit is contained in:
张翔
2026-03-28 14:37:29 +08:00
commit 08ea5fbe98
1643 changed files with 255646 additions and 0 deletions
+239
View File
@@ -0,0 +1,239 @@
# Everything is Suitable API Test
API 测试模块,基于 Python + pytest 框架实现,提供完整的 API 黑盒测试解决方案。
## 功能特性
- 测试数据管理:支持JSON、CSV等多种格式的测试用例与测试数据存储
- 内存数据存储:使用内存数据结构管理测试数据,无需数据库
- API测试功能:支持RESTful API的所有HTTP方法(GET、POST、PUT、DELETE等)
- 请求构造:支持请求参数构造、请求头配置、认证授权处理
- 响应验证:支持状态码检查、响应体断言、响应时间阈值验证
- 依赖处理:支持API依赖关系处理与测试用例执行顺序控制
- 测试报告:支持HTML和JSON格式的测试报告,包含可视化图表
- 命令行接口:提供完整的命令行接口,支持测试套件指定和过滤
- 日志记录:实现详细的日志记录系统
## 技术栈
- Python 3.10+
- Poetry(依赖管理)
- requests/httpxHTTP客户端)
- pytest(测试框架)
- Jinja2(模板引擎)
- PyYAML(配置文件解析)
- Click(命令行框架)
## 项目结构
```
api/
├── src/apitest/ # API测试源代码
│ ├── models/ # 数据模型
│ │ ├── __init__.py
│ │ ├── exceptions.py # 异常定义
│ │ └── test_models.py # 测试数据模型
│ ├── client/ # HTTP客户端
│ │ ├── __init__.py
│ │ ├── api_client.py # API客户端
│ │ └── auth_manager.py # 认证管理器
│ ├── config/ # 配置管理
│ │ ├── __init__.py
│ │ ├── config_manager.py # 配置管理器
│ │ └── logger_manager.py # 日志管理器
│ ├── core/ # 核心功能
│ │ ├── __init__.py
│ │ ├── test_engine.py # 测试引擎
│ │ └── validation_engine.py # 验证引擎
│ ├── data/ # 数据管理
│ │ ├── __init__.py
│ │ └── test_data_manager.py # 测试数据管理器
│ ├── report/ # 报告生成
│ │ ├── __init__.py
│ │ └── report_manager.py # 报告管理器
│ ├── orchestrator/ # 测试编排
│ │ ├── __init__.py
│ │ └── test_orchestrator.py # 测试编排器
│ ├── utils/ # 工具类
│ │ └── __init__.py
│ ├── cli/ # 命令行接口
│ │ ├── __init__.py
│ │ └── cli_module.py # CLI模块
│ ├── __init__.py
│ ├── cli_module.py # CLI入口
│ └── main.py # 主入口
├── test_cases/ # 测试用例
│ ├── example_test_cases.json
│ ├── example_test_data.csv
│ └── parameterized_test_cases.json
├── data/ # 测试数据
│ └── .gitkeep
├── config/ # 配置文件
│ └── config.yaml
├── tests/ # 测试代码
│ ├── unit/ # 单元测试
│ │ ├── test_cli.py
│ │ ├── test_config_manager.py
│ │ ├── test_logger_manager.py
│ │ ├── test_models.py
│ │ ├── test_report_manager.py
│ │ ├── test_test_data_manager.py
│ │ ├── test_test_engine.py
│ │ ├── test_test_orchestrator.py
│ │ └── test_validation_engine.py
│ └── integration/ # 集成测试
├── pyproject.toml # Poetry配置
├── requirements.txt # Python依赖
├── setup.py # Python安装脚本
└── README.md # 本文档
```
## 安装
### 前置要求
- Python 3.10+
- Poetry 1.6.0+
### 安装步骤
```bash
# 进入 API 测试目录
cd api
# 安装依赖
poetry install
# 或者使用 requirements.txt
pip install -r requirements.txt
```
### 配置环境变量
在项目根目录的 `.env` 文件中配置 API 测试相关环境变量:
```env
# API测试环境配置
API_BASE_URL=http://localhost:8080
API_TIMEOUT=30000
API_MAX_RETRIES=3
# 认证配置
TEST_USERNAME=admin
TEST_PASSWORD=admin123
# 测试配置
TEST_PARALLEL=true
TEST_RETRY_COUNT=3
TEST_LOG_LEVEL=INFO
```
## 使用
### 基本命令
```bash
# 运行所有 API 测试
npm run test:api
# 运行 API 单元测试
npm run test:api:unit
# 并行运行 API 测试
npm run test:api:parallel
# 生成 API 测试报告
npm run test:api:report
# 格式化 API 测试代码
npm run format:api
```
### 配置文件
配置文件位于 `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
## 与 E2E 测试的集成
API 测试模块已整合到统一测试平台中,可以与 E2E 测试一起运行:
```bash
# 运行所有测试(E2E + API
npm test
# 查看统一测试报告
npm run test:report
```
## 开发
### 代码规范
- 遵循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/
```
## 测试报告
API 测试报告使用 Allure 框架生成,支持:
- HTML 格式报告
- JSON 格式报告
- 测试用例详情
- 测试趋势分析
- 可视化图表
报告生成在 `../test-results/api/allure-report/` 目录下。
## 贡献
欢迎贡献代码!请阅读 [CONTRIBUTING.md](../../CONTRIBUTING.md) 了解贡献指南。
## 许可证
MIT License
## 联系方式
- 项目主页: https://github.com/yourusername/everything-is-suitable
- 问题反馈: https://github.com/yourusername/everything-is-suitable/issues
- 邮箱: test@example.com
@@ -0,0 +1,45 @@
# 目标系统配置
target:
base_url: ${API_BASE_URL}
timeout: ${API_TIMEOUT}
max_retries: ${API_MAX_RETRIES}
# 认证配置
auth:
login_endpoint: /sys/auth/login
username: ${TEST_USERNAME}
password: ${TEST_PASSWORD}
token_storage: memory
token_refresh: true
# 测试配置
test:
data_dir: data
test_cases_dir: test_cases
parallel: ${TEST_PARALLEL}
parallel_threads: 4
retry_count: ${TEST_RETRY_COUNT}
stop_on_failure: false
max_response_time: 5000
# 报告配置
report:
output_dir: ../test-results/api
formats:
- html
- json
include_details: true
include_charts: true
# 日志配置
logging:
level: ${TEST_LOG_LEVEL}
file: ../test-results/api/logs/test.log
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
console: true
# 数据管理配置
data:
load_on_startup: true
auto_refresh: false
cache_enabled: true
File diff suppressed because one or more lines are too long
@@ -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,211 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API测试报告</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: #dc3545;
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>API测试报告</h1>
<p class="timestamp">测试套件: Test Suite</p>
<p class="timestamp">生成时间: 2026-03-07 19:14:57</p>
<div class="summary">
<div class="summary-card">
<h3>总用例数</h3>
<div class="value">2</div>
</div>
<div class="summary-card">
<h3>通过</h3>
<div class="value passed">0</div>
</div>
<div class="summary-card">
<h3>失败</h3>
<div class="value failed">2</div>
</div>
<div class="summary-card">
<h3>跳过</h3>
<div class="value skipped">0</div>
</div>
<div class="summary-card">
<h3>通过率</h3>
<div class="value">0.0%</div>
</div>
<div class="summary-card">
<h3>执行时长</h3>
<div class="value">0.00s</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 0.0%">
0.0%
</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>
<tr>
<td>TC001</td>
<td>测试用例1</td>
<td>test</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
<tr>
<td>TC002</td>
<td>测试用例2</td>
<td>user</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,39 @@
{
"suite_name": "Test Suite",
"total": 2,
"passed": 0,
"failed": 2,
"skipped": 0,
"pass_rate": 0.0,
"duration": 0.0,
"start_time": "2026-03-07T19:14:57.182797",
"end_time": null,
"results": [
{
"test_case_id": "TC001",
"test_case_name": "测试用例1",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-07T19:14:57.181895"
},
{
"test_case_id": "TC002",
"test_case_name": "测试用例2",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-07T19:14:57.182784"
}
]
}
@@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API测试报告</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: #dc3545;
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>API测试报告</h1>
<p class="timestamp">测试套件: Test Suite</p>
<p class="timestamp">生成时间: 2026-03-07 19:15:14</p>
<div class="summary">
<div class="summary-card">
<h3>总用例数</h3>
<div class="value">2</div>
</div>
<div class="summary-card">
<h3>通过</h3>
<div class="value passed">0</div>
</div>
<div class="summary-card">
<h3>失败</h3>
<div class="value failed">2</div>
</div>
<div class="summary-card">
<h3>跳过</h3>
<div class="value skipped">0</div>
</div>
<div class="summary-card">
<h3>通过率</h3>
<div class="value">0.0%</div>
</div>
<div class="summary-card">
<h3>执行时长</h3>
<div class="value">0.00s</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 0.0%">
0.0%
</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>
<tr>
<td>TC001</td>
<td>测试用例1</td>
<td>test</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
<tr>
<td>TC002</td>
<td>测试用例2</td>
<td>user</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,39 @@
{
"suite_name": "Test Suite",
"total": 2,
"passed": 0,
"failed": 2,
"skipped": 0,
"pass_rate": 0.0,
"duration": 0.0,
"start_time": "2026-03-07T19:15:14.806296",
"end_time": null,
"results": [
{
"test_case_id": "TC001",
"test_case_name": "测试用例1",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-07T19:15:14.805421"
},
{
"test_case_id": "TC002",
"test_case_name": "测试用例2",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-07T19:15:14.806284"
}
]
}
@@ -0,0 +1,201 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API测试报告</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: #dc3545;
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>API测试报告</h1>
<p class="timestamp">测试套件: Test Suite</p>
<p class="timestamp">生成时间: 2026-03-07 19:15:30</p>
<div class="summary">
<div class="summary-card">
<h3>总用例数</h3>
<div class="value">2</div>
</div>
<div class="summary-card">
<h3>通过</h3>
<div class="value passed">0</div>
</div>
<div class="summary-card">
<h3>失败</h3>
<div class="value failed">1</div>
</div>
<div class="summary-card">
<h3>跳过</h3>
<div class="value skipped">1</div>
</div>
<div class="summary-card">
<h3>通过率</h3>
<div class="value">0.0%</div>
</div>
<div class="summary-card">
<h3>执行时长</h3>
<div class="value">0.00s</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 0.0%">
0.0%
</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>
<tr>
<td>TC001</td>
<td>测试用例1</td>
<td>test</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,39 @@
{
"suite_name": "Test Suite",
"total": 2,
"passed": 0,
"failed": 2,
"skipped": 0,
"pass_rate": 0.0,
"duration": 0.0,
"start_time": "2026-03-07T19:15:30.992094",
"end_time": null,
"results": [
{
"test_case_id": "TC001",
"test_case_name": "测试用例1",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-07T19:15:30.991363"
},
{
"test_case_id": "TC002",
"test_case_name": "测试用例2",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-07T19:15:30.992085"
}
]
}
@@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API测试报告</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: #dc3545;
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>API测试报告</h1>
<p class="timestamp">测试套件: Test Suite</p>
<p class="timestamp">生成时间: 2026-03-07 19:15:31</p>
<div class="summary">
<div class="summary-card">
<h3>总用例数</h3>
<div class="value">2</div>
</div>
<div class="summary-card">
<h3>通过</h3>
<div class="value passed">0</div>
</div>
<div class="summary-card">
<h3>失败</h3>
<div class="value failed">2</div>
</div>
<div class="summary-card">
<h3>跳过</h3>
<div class="value skipped">0</div>
</div>
<div class="summary-card">
<h3>通过率</h3>
<div class="value">0.0%</div>
</div>
<div class="summary-card">
<h3>执行时长</h3>
<div class="value">0.00s</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 0.0%">
0.0%
</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>
<tr>
<td>TC001</td>
<td>测试用例1</td>
<td>test</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
<tr>
<td>TC002</td>
<td>测试用例2</td>
<td>user</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API测试报告</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: #dc3545;
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>API测试报告</h1>
<p class="timestamp">测试套件: Test Suite</p>
<p class="timestamp">生成时间: 2026-03-07 19:20:10</p>
<div class="summary">
<div class="summary-card">
<h3>总用例数</h3>
<div class="value">2</div>
</div>
<div class="summary-card">
<h3>通过</h3>
<div class="value passed">0</div>
</div>
<div class="summary-card">
<h3>失败</h3>
<div class="value failed">2</div>
</div>
<div class="summary-card">
<h3>跳过</h3>
<div class="value skipped">0</div>
</div>
<div class="summary-card">
<h3>通过率</h3>
<div class="value">0.0%</div>
</div>
<div class="summary-card">
<h3>执行时长</h3>
<div class="value">0.00s</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 0.0%">
0.0%
</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>
<tr>
<td>TC001</td>
<td>测试用例1</td>
<td>test</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
<tr>
<td>TC002</td>
<td>测试用例2</td>
<td>user</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,39 @@
{
"suite_name": "Test Suite",
"total": 2,
"passed": 0,
"failed": 2,
"skipped": 0,
"pass_rate": 0.0,
"duration": 0.0,
"start_time": "2026-03-07T19:20:10.082546",
"end_time": null,
"results": [
{
"test_case_id": "TC001",
"test_case_name": "测试用例1",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-07T19:20:10.081808"
},
{
"test_case_id": "TC002",
"test_case_name": "测试用例2",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-07T19:20:10.082536"
}
]
}
@@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API测试报告</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: #dc3545;
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>API测试报告</h1>
<p class="timestamp">测试套件: Test Suite</p>
<p class="timestamp">生成时间: 2026-03-07 19:23:00</p>
<div class="summary">
<div class="summary-card">
<h3>总用例数</h3>
<div class="value">2</div>
</div>
<div class="summary-card">
<h3>通过</h3>
<div class="value passed">0</div>
</div>
<div class="summary-card">
<h3>失败</h3>
<div class="value failed">2</div>
</div>
<div class="summary-card">
<h3>跳过</h3>
<div class="value skipped">0</div>
</div>
<div class="summary-card">
<h3>通过率</h3>
<div class="value">0.0%</div>
</div>
<div class="summary-card">
<h3>执行时长</h3>
<div class="value">0.00s</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 0.0%">
0.0%
</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>
<tr>
<td>TC001</td>
<td>测试用例1</td>
<td>test</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
<tr>
<td>TC002</td>
<td>测试用例2</td>
<td>user</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,39 @@
{
"suite_name": "Test Suite",
"total": 2,
"passed": 0,
"failed": 2,
"skipped": 0,
"pass_rate": 0.0,
"duration": 0.0,
"start_time": "2026-03-07T19:23:00.758738",
"end_time": null,
"results": [
{
"test_case_id": "TC001",
"test_case_name": "测试用例1",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-07T19:23:00.758034"
},
{
"test_case_id": "TC002",
"test_case_name": "测试用例2",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-07T19:23:00.758730"
}
]
}
@@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API测试报告</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: #dc3545;
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>API测试报告</h1>
<p class="timestamp">测试套件: Test Suite</p>
<p class="timestamp">生成时间: 2026-03-07 22:18:17</p>
<div class="summary">
<div class="summary-card">
<h3>总用例数</h3>
<div class="value">2</div>
</div>
<div class="summary-card">
<h3>通过</h3>
<div class="value passed">0</div>
</div>
<div class="summary-card">
<h3>失败</h3>
<div class="value failed">2</div>
</div>
<div class="summary-card">
<h3>跳过</h3>
<div class="value skipped">0</div>
</div>
<div class="summary-card">
<h3>通过率</h3>
<div class="value">0.0%</div>
</div>
<div class="summary-card">
<h3>执行时长</h3>
<div class="value">0.00s</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 0.0%">
0.0%
</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>
<tr>
<td>TC001</td>
<td>测试用例1</td>
<td>test</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
<tr>
<td>TC002</td>
<td>测试用例2</td>
<td>user</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,39 @@
{
"suite_name": "Test Suite",
"total": 2,
"passed": 0,
"failed": 2,
"skipped": 0,
"pass_rate": 0.0,
"duration": 0.0,
"start_time": "2026-03-07T22:18:17.329331",
"end_time": null,
"results": [
{
"test_case_id": "TC001",
"test_case_name": "测试用例1",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-07T22:18:17.328611"
},
{
"test_case_id": "TC002",
"test_case_name": "测试用例2",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-07T22:18:17.329323"
}
]
}
@@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API测试报告</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: #dc3545;
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>API测试报告</h1>
<p class="timestamp">测试套件: Test Suite</p>
<p class="timestamp">生成时间: 2026-03-07 22:34:06</p>
<div class="summary">
<div class="summary-card">
<h3>总用例数</h3>
<div class="value">2</div>
</div>
<div class="summary-card">
<h3>通过</h3>
<div class="value passed">0</div>
</div>
<div class="summary-card">
<h3>失败</h3>
<div class="value failed">2</div>
</div>
<div class="summary-card">
<h3>跳过</h3>
<div class="value skipped">0</div>
</div>
<div class="summary-card">
<h3>通过率</h3>
<div class="value">0.0%</div>
</div>
<div class="summary-card">
<h3>执行时长</h3>
<div class="value">0.00s</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 0.0%">
0.0%
</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>
<tr>
<td>TC001</td>
<td>测试用例1</td>
<td>test</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
<tr>
<td>TC002</td>
<td>测试用例2</td>
<td>user</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,39 @@
{
"suite_name": "Test Suite",
"total": 2,
"passed": 0,
"failed": 2,
"skipped": 0,
"pass_rate": 0.0,
"duration": 0.0,
"start_time": "2026-03-07T22:34:06.481595",
"end_time": null,
"results": [
{
"test_case_id": "TC001",
"test_case_name": "测试用例1",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-07T22:34:06.479215"
},
{
"test_case_id": "TC002",
"test_case_name": "测试用例2",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-07T22:34:06.481553"
}
]
}
@@ -0,0 +1,201 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API测试报告</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: #dc3545;
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>API测试报告</h1>
<p class="timestamp">测试套件: Test Suite</p>
<p class="timestamp">生成时间: 2026-03-07 23:23:44</p>
<div class="summary">
<div class="summary-card">
<h3>总用例数</h3>
<div class="value">2</div>
</div>
<div class="summary-card">
<h3>通过</h3>
<div class="value passed">0</div>
</div>
<div class="summary-card">
<h3>失败</h3>
<div class="value failed">1</div>
</div>
<div class="summary-card">
<h3>跳过</h3>
<div class="value skipped">1</div>
</div>
<div class="summary-card">
<h3>通过率</h3>
<div class="value">0.0%</div>
</div>
<div class="summary-card">
<h3>执行时长</h3>
<div class="value">0.00s</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 0.0%">
0.0%
</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>
<tr>
<td>TC001</td>
<td>测试用例1</td>
<td>test</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,201 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API测试报告</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: #dc3545;
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>API测试报告</h1>
<p class="timestamp">测试套件: Test Suite</p>
<p class="timestamp">生成时间: 2026-03-07 23:23:45</p>
<div class="summary">
<div class="summary-card">
<h3>总用例数</h3>
<div class="value">1</div>
</div>
<div class="summary-card">
<h3>通过</h3>
<div class="value passed">0</div>
</div>
<div class="summary-card">
<h3>失败</h3>
<div class="value failed">1</div>
</div>
<div class="summary-card">
<h3>跳过</h3>
<div class="value skipped">0</div>
</div>
<div class="summary-card">
<h3>通过率</h3>
<div class="value">0.0%</div>
</div>
<div class="summary-card">
<h3>执行时长</h3>
<div class="value">0.00s</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 0.0%">
0.0%
</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>
<tr>
<td>TC002</td>
<td>测试用例2</td>
<td>user</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,39 @@
{
"suite_name": "Test Suite",
"total": 2,
"passed": 0,
"failed": 2,
"skipped": 0,
"pass_rate": 0.0,
"duration": 0.0,
"start_time": "2026-03-07T23:23:45.123871",
"end_time": null,
"results": [
{
"test_case_id": "TC001",
"test_case_name": "测试用例1",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-07T23:23:45.122486"
},
{
"test_case_id": "TC002",
"test_case_name": "测试用例2",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-07T23:23:45.123826"
}
]
}
@@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API测试报告</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: #dc3545;
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>API测试报告</h1>
<p class="timestamp">测试套件: Test Suite</p>
<p class="timestamp">生成时间: 2026-03-07 23:23:46</p>
<div class="summary">
<div class="summary-card">
<h3>总用例数</h3>
<div class="value">2</div>
</div>
<div class="summary-card">
<h3>通过</h3>
<div class="value passed">0</div>
</div>
<div class="summary-card">
<h3>失败</h3>
<div class="value failed">2</div>
</div>
<div class="summary-card">
<h3>跳过</h3>
<div class="value skipped">0</div>
</div>
<div class="summary-card">
<h3>通过率</h3>
<div class="value">0.0%</div>
</div>
<div class="summary-card">
<h3>执行时长</h3>
<div class="value">0.00s</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 0.0%">
0.0%
</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>
<tr>
<td>TC001</td>
<td>测试用例1</td>
<td>test</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
<tr>
<td>TC002</td>
<td>测试用例2</td>
<td>user</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API测试报告</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: #dc3545;
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>API测试报告</h1>
<p class="timestamp">测试套件: Test Suite</p>
<p class="timestamp">生成时间: 2026-03-08 19:26:48</p>
<div class="summary">
<div class="summary-card">
<h3>总用例数</h3>
<div class="value">2</div>
</div>
<div class="summary-card">
<h3>通过</h3>
<div class="value passed">0</div>
</div>
<div class="summary-card">
<h3>失败</h3>
<div class="value failed">2</div>
</div>
<div class="summary-card">
<h3>跳过</h3>
<div class="value skipped">0</div>
</div>
<div class="summary-card">
<h3>通过率</h3>
<div class="value">0.0%</div>
</div>
<div class="summary-card">
<h3>执行时长</h3>
<div class="value">0.00s</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 0.0%">
0.0%
</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>
<tr>
<td>TC001</td>
<td>测试用例1</td>
<td>test</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
<tr>
<td>TC002</td>
<td>测试用例2</td>
<td>user</td>
<td class="status-fail">失败</td>
<td>0</td>
<td>N/A</td>
<td class="error-message">执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,39 @@
{
"suite_name": "Test Suite",
"total": 2,
"passed": 0,
"failed": 2,
"skipped": 0,
"pass_rate": 0.0,
"duration": 0.0,
"start_time": "2026-03-08T19:26:48.162985",
"end_time": null,
"results": [
{
"test_case_id": "TC001",
"test_case_name": "测试用例1",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-08T19:26:48.160487"
},
{
"test_case_id": "TC002",
"test_case_name": "测试用例2",
"passed": false,
"status_code": 0,
"response_body": null,
"response_headers": {},
"error_message": "执行异常: 登录请求失败: Invalid URL '${API_BASE_URL}/sys/auth/login': No scheme supplied. Perhaps you meant https://${API_BASE_URL}/sys/auth/login?",
"performance": null,
"execution_time": 0.0,
"retry_count": 0,
"timestamp": "2026-03-08T19:26:48.162973"
}
]
}
@@ -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
+35
View File
@@ -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,182 @@
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:
"""获取超时时间(毫秒)"""
timeout = self.get("target.timeout", 5000)
# 处理环境变量占位符未被解析的情况
if isinstance(timeout, str):
try:
return int(timeout)
except (ValueError, TypeError):
return 5000
return timeout
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.orchestrator.test_orchestrator import TestOrchestrator
from apitest.report.report_manager import ReportManager
from apitest.config.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"]
@@ -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,154 @@
[
{
"id": "TC001",
"name": "获取用户信息",
"description": "测试获取指定用户的信息",
"module": "user",
"endpoint": "/api/user/{userId}",
"method": "GET",
"headers": {
"Content-Type": "application/json"
},
"auth_required": true,
"dependencies": [],
"timeout": 5000,
"retry_count": 0,
"validations": [
{
"type": "status_code",
"expected": 200
},
{
"type": "json_path",
"json_path": "$.data.id",
"expected": 1
}
],
"tags": ["smoke", "regression"],
"priority": 0,
"enabled": true
},
{
"id": "TC002",
"name": "创建用户",
"description": "测试创建新用户",
"module": "user",
"endpoint": "/api/user",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": {
"username": "testuser",
"email": "test@example.com",
"password": "password123"
},
"auth_required": true,
"dependencies": [],
"timeout": 5000,
"retry_count": 0,
"validations": [
{
"type": "status_code",
"expected": 201
},
{
"type": "json_path",
"json_path": "$.data.username",
"expected": "testuser"
}
],
"tags": ["smoke"],
"priority": 1,
"enabled": true
},
{
"id": "TC003",
"name": "更新用户信息",
"description": "测试更新用户信息",
"module": "user",
"endpoint": "/api/user/{userId}",
"method": "PUT",
"headers": {
"Content-Type": "application/json"
},
"body": {
"email": "updated@example.com"
},
"auth_required": true,
"dependencies": ["TC001"],
"timeout": 5000,
"retry_count": 0,
"validations": [
{
"type": "status_code",
"expected": 200
},
{
"type": "json_path",
"json_path": "$.data.email",
"expected": "updated@example.com"
}
],
"tags": ["regression"],
"priority": 2,
"enabled": true
},
{
"id": "TC004",
"name": "删除用户",
"description": "测试删除用户",
"module": "user",
"endpoint": "/api/user/{userId}",
"method": "DELETE",
"headers": {
"Content-Type": "application/json"
},
"auth_required": true,
"dependencies": ["TC003"],
"timeout": 5000,
"retry_count": 0,
"validations": [
{
"type": "status_code",
"expected": 204
}
],
"tags": ["regression"],
"priority": 2,
"enabled": true
},
{
"id": "TC005",
"name": "获取用户列表",
"description": "测试获取用户列表",
"module": "user",
"endpoint": "/api/users",
"method": "GET",
"headers": {
"Content-Type": "application/json"
},
"params": {
"page": 1,
"size": 10
},
"auth_required": true,
"dependencies": [],
"timeout": 5000,
"retry_count": 0,
"validations": [
{
"type": "status_code",
"expected": 200
},
{
"type": "json_path",
"json_path": "$.data.length()",
"expected": 10
}
],
"tags": ["smoke"],
"priority": 0,
"enabled": true
}
]
@@ -0,0 +1,4 @@
userId,username,email
1,testuser1,test1@example.com
2,testuser2,test2@example.com
3,testuser3,test3@example.com
1 userId username email
2 1 testuser1 test1@example.com
3 2 testuser2 test2@example.com
4 3 testuser3 test3@example.com
@@ -0,0 +1,26 @@
[
{
"id": "TC006",
"name": "获取用户信息(参数化)",
"description": "测试获取指定用户的信息(参数化测试)",
"module": "user",
"endpoint": "/api/user/{userId}",
"method": "GET",
"headers": {
"Content-Type": "application/json"
},
"auth_required": true,
"dependencies": [],
"timeout": 5000,
"retry_count": 0,
"validations": [
{
"type": "status_code",
"expected": 200
}
],
"tags": ["parameterized"],
"priority": 1,
"enabled": true
}
]
@@ -0,0 +1,245 @@
"""测试API客户端"""
import pytest
import time
from unittest.mock import Mock, patch, MagicMock
import requests
from apitest.client.api_client import APIClient
from apitest.models.exceptions import RequestException
from apitest.models.test_models import HTTPMethod, PerformanceMetrics
class TestAPIClient:
"""测试APIClient类"""
def test_init(self):
"""测试初始化"""
client = APIClient("http://localhost:8080", timeout=5000, max_retries=3)
assert client.base_url == "http://localhost:8080"
assert client.timeout == 5.0
assert client.max_retries == 3
assert "Content-Type" in client._default_headers
assert "Accept" in client._default_headers
def test_init_with_logger(self):
"""测试带日志记录器的初始化"""
logger = Mock()
client = APIClient("http://localhost:8080", logger=logger)
assert client.logger == logger
def test_set_default_headers(self):
"""测试设置默认请求头"""
client = APIClient("http://localhost:8080")
client.set_default_headers({"X-Custom-Header": "value"})
assert "X-Custom-Header" in client._default_headers
assert client._default_headers["X-Custom-Header"] == "value"
def test_set_auth_token(self):
"""测试设置认证token"""
client = APIClient("http://localhost:8080")
client.set_auth_token("test-token")
assert "Authorization" in client._default_headers
assert client._default_headers["Authorization"] == "Bearer test-token"
def test_build_url(self):
"""测试构建URL"""
client = APIClient("http://localhost:8080")
url = client._build_url("/api/test")
assert url == "http://localhost:8080/api/test"
def test_build_url_with_leading_slash(self):
"""测试构建URL(带前导斜杠)"""
client = APIClient("http://localhost:8080")
url = client._build_url("api/test")
assert url == "http://localhost:8080/api/test"
def test_merge_headers(self):
"""测试合并请求头"""
client = APIClient("http://localhost:8080")
client.set_default_headers({"X-Default": "default"})
merged = client._merge_headers({"X-Custom": "custom"})
assert merged["X-Default"] == "default"
assert merged["X-Custom"] == "custom"
def test_merge_headers_override(self):
"""测试合并请求头(覆盖)"""
client = APIClient("http://localhost:8080")
client.set_default_headers({"Content-Type": "application/xml"})
merged = client._merge_headers({"Content-Type": "application/json"})
assert merged["Content-Type"] == "application/json"
@patch('requests.Session.get')
def test_request_success(self, mock_get):
"""测试成功请求"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"success": True}
mock_response.headers = {"Content-Type": "application/json"}
mock_get.return_value = mock_response
client = APIClient("http://localhost:8080")
result = client.request(HTTPMethod.GET, "/api/test")
assert result["status_code"] == 200
assert result["response_body"] == {"success": True}
assert "performance" in result
@patch('requests.Session.get')
def test_request_with_params(self, mock_get):
"""测试带参数的请求"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"success": True}
mock_response.headers = {"Content-Type": "application/json"}
mock_get.return_value = mock_response
client = APIClient("http://localhost:8080")
result = client.request(HTTPMethod.GET, "/api/test", params={"page": 1, "size": 10})
assert result["status_code"] == 200
assert result["response_body"] == {"success": True}
mock_get.assert_called_once()
@patch('requests.Session.post')
def test_request_with_body(self, mock_post):
"""测试带请求体的请求"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"success": True}
mock_response.headers = {"Content-Type": "application/json"}
mock_post.return_value = mock_response
client = APIClient("http://localhost:8080")
result = client.request(HTTPMethod.POST, "/api/test", body={"name": "test"})
assert result["status_code"] == 200
assert result["response_body"] == {"success": True}
mock_post.assert_called_once()
@patch('requests.Session.get')
def test_request_with_headers(self, mock_get):
"""测试带自定义请求头的请求"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"success": True}
mock_response.headers = {"Content-Type": "application/json"}
mock_get.return_value = mock_response
client = APIClient("http://localhost:8080")
result = client.request(HTTPMethod.GET, "/api/test", headers={"X-Custom": "value"})
assert result["status_code"] == 200
assert result["response_body"] == {"success": True}
mock_get.assert_called_once()
@patch('requests.Session.get')
def test_request_retry_on_timeout(self, mock_get):
"""测试超时重试"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"success": True}
mock_response.headers = {"Content-Type": "application/json"}
mock_get.side_effect = [
requests.Timeout(),
requests.Timeout(),
mock_response
]
client = APIClient("http://localhost:8080", max_retries=3)
result = client.request(HTTPMethod.GET, "/api/test")
assert result["status_code"] == 200
assert result["response_body"] == {"success": True}
assert mock_get.call_count == 3
@patch('requests.Session.get')
def test_request_failure_after_retries(self, mock_get):
"""测试重试后仍然失败"""
mock_get.side_effect = requests.Timeout()
client = APIClient("http://localhost:8080", max_retries=2)
with pytest.raises(RequestException):
client.request(HTTPMethod.GET, "/api/test")
@patch('requests.Session.get')
def test_request_invalid_json(self, mock_get):
"""测试无效JSON响应"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.side_effect = ValueError("Invalid JSON")
mock_response.text = "plain text"
mock_response.headers = {"Content-Type": "text/plain"}
mock_get.return_value = mock_response
client = APIClient("http://localhost:8080")
result = client.request(HTTPMethod.GET, "/api/test")
assert result["status_code"] == 200
assert result["response_body"] == "plain text"
@patch('requests.Session.get')
def test_get(self, mock_get):
"""测试GET请求"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"success": True}
mock_response.headers = {"Content-Type": "application/json"}
mock_get.return_value = mock_response
client = APIClient("http://localhost:8080")
result = client.get("/api/test")
assert result["status_code"] == 200
assert result["response_body"] == {"success": True}
@patch('requests.Session.post')
def test_post(self, mock_post):
"""测试POST请求"""
mock_response = Mock()
mock_response.status_code = 201
mock_response.json.return_value = {"success": True}
mock_response.headers = {"Content-Type": "application/json"}
mock_post.return_value = mock_response
client = APIClient("http://localhost:8080")
result = client.post("/api/test", {"name": "test"})
assert result["status_code"] == 201
assert result["response_body"] == {"success": True}
@patch('requests.Session.put')
def test_put(self, mock_put):
"""测试PUT请求"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"success": True}
mock_response.headers = {"Content-Type": "application/json"}
mock_put.return_value = mock_response
client = APIClient("http://localhost:8080")
result = client.put("/api/test/1", {"name": "updated"})
assert result["status_code"] == 200
assert result["response_body"] == {"success": True}
@patch('requests.Session.delete')
def test_delete(self, mock_delete):
"""测试DELETE请求"""
mock_response = Mock()
mock_response.status_code = 204
mock_response.headers = {"Content-Type": "application/json"}
mock_delete.return_value = mock_response
client = APIClient("http://localhost:8080")
result = client.delete("/api/test/1")
assert result["status_code"] == 204
def test_close(self):
"""测试关闭客户端"""
client = APIClient("http://localhost:8080")
session = client._session
client.close()
assert client._session == session
@@ -0,0 +1,370 @@
"""测试认证管理器"""
import pytest
from unittest.mock import Mock, patch, MagicMock
from apitest.client.auth_manager import AuthManager
from apitest.models.exceptions import AuthException
import time
class TestAuthManager:
"""测试AuthManager类"""
def test_init(self):
"""测试初始化"""
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
assert auth_manager.base_url == "http://localhost:8080"
assert auth_manager.credentials == credentials
assert auth_manager.logger == logger
assert auth_manager._token is None
assert auth_manager._refresh_token is None
assert auth_manager._token_expiry is None
assert auth_manager._login_endpoint == "/sys/auth/login"
def test_set_login_endpoint(self):
"""测试设置登录端点"""
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
auth_manager.set_login_endpoint("/api/custom/login")
assert auth_manager._login_endpoint == "/api/custom/login"
def test_set_credentials(self):
"""测试设置认证凭据"""
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
auth_manager.set_credentials("newuser", "newpassword")
assert auth_manager.credentials == {"username": "newuser", "password": "newpassword"}
def test_set_token(self):
"""测试设置token"""
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
auth_manager.set_token("test-token", 3600)
assert auth_manager._token == "test-token"
assert auth_manager._token_expiry is not None
def test_get_token(self):
"""测试获取token"""
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
auth_manager._token = "test-token"
assert auth_manager.get_token() == "test-token"
def test_get_token_none(self):
"""测试获取token(未设置)"""
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
assert auth_manager.get_token() is None
def test_is_token_valid(self):
"""测试token有效性检查"""
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
auth_manager.set_token("test-token", 3600)
assert auth_manager.is_token_valid() is True
def test_is_token_valid_expired(self):
"""测试token有效性检查(已过期)"""
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
auth_manager.set_token("test-token", -1)
assert auth_manager.is_token_valid() is False
def test_is_token_valid_none(self):
"""测试token有效性检查(未设置)"""
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
assert auth_manager.is_token_valid() is False
@patch('requests.post')
def test_login_success(self, mock_post):
"""测试成功登录"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"data": {
"token": "test-token",
"refreshToken": "refresh-token",
"expiresIn": 3600
}
}
mock_post.return_value = mock_response
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
result = auth_manager.login()
assert result["data"]["token"] == "test-token"
assert auth_manager._token == "test-token"
assert auth_manager._refresh_token == "refresh-token"
assert auth_manager._token_expiry is not None
@patch('requests.post')
def test_login_failure(self, mock_post):
"""测试登录失败"""
mock_response = Mock()
mock_response.status_code = 401
mock_response.json.return_value = {
"data": {"error": "Invalid credentials"}
}
mock_post.return_value = mock_response
logger = Mock()
credentials = {"username": "admin", "password": "wrong-password"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
with pytest.raises(AuthException):
auth_manager.login()
@patch('requests.post')
def test_login_no_token_in_response(self, mock_post):
"""测试登录(响应中无token"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"data": {"message": "success"}
}
mock_post.return_value = mock_response
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
with pytest.raises(AuthException):
auth_manager.login()
@patch('requests.post')
def test_login_http_error(self, mock_post):
"""测试登录(HTTP错误)"""
import requests
mock_post.side_effect = requests.RequestException("Network error")
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
with pytest.raises(AuthException):
auth_manager.login()
@patch('requests.post')
def test_refresh_token_success(self, mock_post):
"""测试成功刷新token"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"data": {
"token": "new-token",
"refreshToken": "new-refresh-token",
"expiresIn": 3600
}
}
mock_post.return_value = mock_response
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
auth_manager._refresh_token = "old-refresh-token"
result = auth_manager.refresh_token()
assert result is True
assert auth_manager._token == "new-token"
assert auth_manager._refresh_token == "new-refresh-token"
@patch('requests.post')
def test_refresh_token_failure(self, mock_post):
"""测试刷新token失败"""
mock_response = Mock()
mock_response.status_code = 401
mock_response.json.return_value = {
"data": {"error": "Invalid refresh token"}
}
mock_post.return_value = mock_response
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
auth_manager._refresh_token = "old-refresh-token"
result = auth_manager.refresh_token()
assert result is False
def test_refresh_token_no_refresh_token(self):
"""测试刷新token(无refresh token"""
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
result = auth_manager.refresh_token()
assert result is False
@patch('requests.post')
def test_refresh_token_exception(self, mock_post):
"""测试刷新token异常"""
mock_post.side_effect = Exception("Network error")
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
auth_manager._refresh_token = "old-refresh-token"
result = auth_manager.refresh_token()
assert result is False
def test_logout(self):
"""测试登出"""
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
auth_manager._token = "test-token"
auth_manager._refresh_token = "refresh-token"
auth_manager._token_expiry = time.time() + 3600
auth_manager.logout()
assert auth_manager._token is None
assert auth_manager._refresh_token is None
assert auth_manager._token_expiry is None
@patch('requests.post')
def test_ensure_authenticated_with_valid_token(self, mock_post):
"""测试确保已认证(有效token"""
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
auth_manager.set_token("test-token", 3600)
token = auth_manager.ensure_authenticated()
assert token == "test-token"
assert mock_post.call_count == 0
@patch('requests.post')
def test_ensure_authenticated_with_expired_token(self, mock_post):
"""测试确保已认证(过期token,刷新成功)"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"data": {
"token": "new-token",
"refreshToken": "new-refresh-token",
"expiresIn": 3600
}
}
mock_post.return_value = mock_response
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
auth_manager._token = "old-token"
auth_manager._refresh_token = "old-refresh-token"
from datetime import datetime, timedelta
auth_manager._token_expiry = datetime.now() - timedelta(seconds=100)
token = auth_manager.ensure_authenticated()
assert token == "new-token"
assert auth_manager._token == "new-token"
assert auth_manager._refresh_token == "new-refresh-token"
@patch('requests.post')
def test_ensure_authenticated_no_token(self, mock_post):
"""测试确保已认证(无token,登录成功)"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"data": {
"token": "test-token",
"refreshToken": "refresh-token",
"expiresIn": 3600
}
}
mock_post.return_value = mock_response
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
token = auth_manager.ensure_authenticated()
assert token == "test-token"
@patch('requests.post')
def test_ensure_authenticated_refresh_failed_login(self, mock_post):
"""测试确保已认证(刷新失败,重新登录)"""
refresh_response = Mock()
refresh_response.status_code = 401
refresh_response.json.return_value = {
"data": {"error": "Invalid refresh token"}
}
login_response = Mock()
login_response.status_code = 200
login_response.json.return_value = {
"data": {
"token": "test-token",
"refreshToken": "refresh-token",
"expiresIn": 3600
}
}
mock_post.side_effect = [refresh_response, login_response]
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
auth_manager._token = "old-token"
auth_manager._refresh_token = "old-refresh-token"
from datetime import datetime, timedelta
auth_manager._token_expiry = datetime.now() - timedelta(seconds=100)
token = auth_manager.ensure_authenticated()
assert token == "test-token"
assert auth_manager._token == "test-token"
@patch('requests.post')
def test_ensure_authenticated_all_failed(self, mock_post):
"""测试确保已认证(全部失败)"""
mock_response = Mock()
mock_response.status_code = 401
mock_response.json.return_value = {
"data": {"error": "Authentication failed"}
}
mock_post.return_value = mock_response
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
with pytest.raises(AuthException):
auth_manager.ensure_authenticated()
@patch('requests.post')
def test_get_auth_headers(self, mock_post):
"""测试获取认证请求头"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"data": {
"token": "test-token",
"refreshToken": "refresh-token",
"expiresIn": 3600
}
}
mock_post.return_value = mock_response
logger = Mock()
credentials = {"username": "admin", "password": "password123"}
auth_manager = AuthManager("http://localhost:8080", credentials, logger)
headers = auth_manager.get_auth_headers()
assert headers["Authorization"] == "Bearer test-token"
assert headers["Content-Type"] == "application/json"
@@ -0,0 +1,383 @@
"""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 or "错误:" 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
def test_run_command_with_verbose(runner, sample_test_cases):
"""测试运行命令(详细模式)"""
result = runner.invoke(cli, ["run", "--test-cases", str(sample_test_cases), "--verbose"])
assert result.exit_code in [0, 1]
def test_run_command_with_stop_on_failure(runner, sample_test_cases):
"""测试运行命令(失败时停止)"""
result = runner.invoke(cli, ["run", "--test-cases", str(sample_test_cases), "--stop-on-failure"])
assert result.exit_code in [0, 1]
def test_run_command_with_no_report(runner, sample_test_cases):
"""测试运行命令(不生成报告)"""
result = runner.invoke(cli, ["run", "--test-cases", str(sample_test_cases), "--no-report"])
assert result.exit_code in [0, 1]
def test_run_command_with_report_format_json(runner, sample_test_cases):
"""测试运行命令(JSON报告格式)"""
result = runner.invoke(cli, ["run", "--test-cases", str(sample_test_cases), "--report-format", "json"])
assert result.exit_code in [0, 1]
def test_run_command_with_report_path(runner, sample_test_cases, tmp_path):
"""测试运行命令(指定报告路径)"""
report_path = tmp_path / "report.html"
result = runner.invoke(cli, ["run", "--test-cases", str(sample_test_cases), "--report-path", str(report_path)])
assert result.exit_code in [0, 1]
def test_run_command_with_module_filter(runner, sample_test_cases):
"""测试运行命令(模块过滤)"""
result = runner.invoke(cli, ["run", "--test-cases", str(sample_test_cases), "--module", "test"])
assert result.exit_code in [0, 1]
def test_run_command_with_tag_filter(runner, sample_test_cases):
"""测试运行命令(标签过滤)"""
result = runner.invoke(cli, ["run", "--test-cases", str(sample_test_cases), "--tag", "smoke"])
assert result.exit_code in [0, 1]
def test_run_command_with_priority_filter(runner, sample_test_cases):
"""测试运行命令(优先级过滤)"""
result = runner.invoke(cli, ["run", "--test-cases", str(sample_test_cases), "--priority", "1"])
assert result.exit_code in [0, 1]
def test_run_command_with_no_matching_cases(runner, sample_test_cases):
"""测试运行命令(无匹配测试用例)"""
result = runner.invoke(cli, ["run", "--test-cases", str(sample_test_cases), "--module", "nonexistent"])
assert result.exit_code == 0
assert "没有匹配的测试用例" in result.output or "警告" in result.output
def test_run_command_with_test_data(runner, sample_test_cases, tmp_path):
"""测试运行命令(带测试数据)"""
test_data = [
{"username": "test1", "password": "pass1"},
{"username": "test2", "password": "pass2"}
]
test_data_file = tmp_path / "test_data.csv"
with open(test_data_file, "w", encoding="utf-8") as f:
f.write("username,password\n")
f.write("test1,pass1\n")
f.write("test2,pass2\n")
result = runner.invoke(cli, ["run", "--test-cases", str(sample_test_cases), "--test-data", str(test_data_file)])
assert result.exit_code in [0, 1]
def test_run_command_with_exception(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, ["run", "--test-cases", str(invalid_file)])
assert result.exit_code == 1
assert "执行测试时出错" in result.output or "错误:" in result.output
def test_run_command_with_verbose_exception(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, ["run", "--test-cases", str(invalid_file), "--verbose"])
assert result.exit_code == 1
def test_list_command_with_exception(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, ["list", str(invalid_file)])
assert result.exit_code == 1
assert "列出测试用例时出错" in result.output or "错误:" in result.output
def test_validate_command_with_missing_fields(runner, tmp_path):
"""测试验证命令(缺少字段)"""
invalid_test_data = [
{
"id": "",
"name": "测试用例",
"endpoint": "/api/test",
"method": "GET"
}
]
invalid_file = tmp_path / "invalid.json"
with open(invalid_file, "w", encoding="utf-8") as f:
json.dump(invalid_test_data, f)
result = runner.invoke(cli, ["validate", str(invalid_file)])
assert result.exit_code == 1
assert "验证失败" in result.output
def test_validate_command_with_missing_id(runner, tmp_path):
"""测试验证命令(缺少ID"""
invalid_test_data = [
{
"name": "测试用例",
"endpoint": "/api/test",
"method": "GET"
}
]
invalid_file = tmp_path / "invalid.json"
with open(invalid_file, "w", encoding="utf-8") as f:
json.dump(invalid_test_data, f)
result = runner.invoke(cli, ["validate", str(invalid_file)])
assert result.exit_code == 1
def test_validate_command_with_missing_name(runner, tmp_path):
"""测试验证命令(缺少名称)"""
invalid_test_data = [
{
"id": "TC001",
"endpoint": "/api/test",
"method": "GET"
}
]
invalid_file = tmp_path / "invalid.json"
with open(invalid_file, "w", encoding="utf-8") as f:
json.dump(invalid_test_data, f)
result = runner.invoke(cli, ["validate", str(invalid_file)])
assert result.exit_code == 1
def test_validate_command_with_missing_endpoint(runner, tmp_path):
"""测试验证命令(缺少端点)"""
invalid_test_data = [
{
"id": "TC001",
"name": "测试用例",
"method": "GET"
}
]
invalid_file = tmp_path / "invalid.json"
with open(invalid_file, "w", encoding="utf-8") as f:
json.dump(invalid_test_data, f)
result = runner.invoke(cli, ["validate", str(invalid_file)])
assert result.exit_code == 1
def test_validate_command_with_exception(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 == 1
assert "验证测试用例时出错" in result.output or "错误:" in result.output
def test_config_command_with_exception(runner):
"""测试配置命令(异常处理)"""
result = runner.invoke(cli, ["config", "--key", "nonexistent.key"])
assert result.exit_code in [0, 1]
def test_list_command_with_tags_and_dependencies(runner, tmp_path):
"""测试列出命令(显示标签和依赖)"""
test_data = [
{
"id": "TC001",
"name": "测试用例1",
"description": "测试用例1",
"module": "test",
"endpoint": "/api/test1",
"method": "GET",
"headers": {},
"enabled": True,
"tags": ["smoke", "regression"],
"dependencies": ["TC000"]
}
]
test_file = tmp_path / "test_cases.json"
with open(test_file, "w", encoding="utf-8") as f:
json.dump(test_data, f)
result = runner.invoke(cli, ["list", str(test_file)])
assert result.exit_code == 0
assert "标签" in result.output
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,306 @@
import pytest
from click.testing import CliRunner
from unittest.mock import patch, MagicMock
from apitest.main import cli
import sys
def test_cli_version():
"""测试版本信息"""
runner = CliRunner()
result = runner.invoke(cli, ['--version'])
assert result.exit_code == 0
assert '1.0.0' in result.output
def test_cli_with_no_command():
"""测试无命令时显示帮助信息"""
runner = CliRunner()
result = runner.invoke(cli, [])
assert result.exit_code == 0
assert '黑盒API测试工具' in result.output
def test_cli_run_with_valid_options():
"""测试有效命令执行"""
runner = CliRunner()
with patch('apitest.main.ConfigManager') as mock_config:
with patch('apitest.main.LoggerManager') as mock_logger:
with patch('apitest.main.TestOrchestrator') as mock_orchestrator:
with patch('apitest.main.ReportManager') as mock_report:
mock_config.return_value = MagicMock()
mock_logger.return_value = MagicMock()
mock_orchestrator.return_value = MagicMock()
mock_report.return_value = MagicMock()
mock_results = MagicMock()
mock_results.passed = 10
mock_results.failed = 0
mock_results.skipped = 0
mock_orchestrator.return_value.run_suite.return_value = mock_results
result = runner.invoke(cli, ['run', '--suite', 'test-suite'])
assert result.exit_code == 0
assert mock_orchestrator.return_value.run_suite.called
def test_cli_run_with_filter():
"""测试带过滤器的运行命令"""
runner = CliRunner()
with patch('apitest.main.ConfigManager') as mock_config:
with patch('apitest.main.LoggerManager') as mock_logger:
with patch('apitest.main.TestOrchestrator') as mock_orchestrator:
with patch('apitest.main.ReportManager') as mock_report:
mock_config.return_value = MagicMock()
mock_logger.return_value = MagicMock()
mock_orchestrator.return_value = MagicMock()
mock_report.return_value = MagicMock()
mock_results = MagicMock()
mock_results.passed = 5
mock_results.failed = 0
mock_results.skipped = 0
mock_orchestrator.return_value.run_suite.return_value = mock_results
result = runner.invoke(cli, ['run', '--filter', 'priority=high'])
assert result.exit_code == 0
def test_cli_run_with_parallel():
"""测试并发执行"""
runner = CliRunner()
with patch('apitest.main.ConfigManager') as mock_config:
with patch('apitest.main.LoggerManager') as mock_logger:
with patch('apitest.main.TestOrchestrator') as mock_orchestrator:
with patch('apitest.main.ReportManager') as mock_report:
mock_config.return_value = MagicMock()
mock_logger.return_value = MagicMock()
mock_orchestrator.return_value = MagicMock()
mock_report.return_value = MagicMock()
mock_results = MagicMock()
mock_results.passed = 10
mock_results.failed = 0
mock_results.skipped = 0
mock_orchestrator.return_value.run_suite.return_value = mock_results
result = runner.invoke(cli, ['run', '--parallel', '--threads', '8'])
assert result.exit_code == 0
def test_cli_run_with_failed_tests():
"""测试有失败测试的情况"""
runner = CliRunner()
with patch('apitest.main.ConfigManager') as mock_config:
with patch('apitest.main.LoggerManager') as mock_logger:
with patch('apitest.main.TestOrchestrator') as mock_orchestrator:
with patch('apitest.main.ReportManager') as mock_report:
mock_config.return_value = MagicMock()
mock_logger.return_value = MagicMock()
mock_orchestrator.return_value = MagicMock()
mock_report.return_value = MagicMock()
mock_results = MagicMock()
mock_results.passed = 5
mock_results.failed = 2
mock_results.skipped = 0
mock_orchestrator.return_value.run_suite.return_value = mock_results
result = runner.invoke(cli, ['run'])
assert result.exit_code == 1
def test_cli_run_with_exception():
"""测试异常处理"""
runner = CliRunner()
with patch('apitest.main.ConfigManager') as mock_config:
mock_config.side_effect = Exception("Config error")
result = runner.invoke(cli, ['run'])
assert result.exit_code == 1
assert '执行测试时出错' in result.output
def test_cli_list_command():
"""测试list命令"""
runner = CliRunner()
with patch('apitest.main.ConfigManager') as mock_config:
with patch('apitest.main.LoggerManager') as mock_logger:
with patch('apitest.main.TestOrchestrator') as mock_orchestrator:
mock_config.return_value = MagicMock()
mock_logger.return_value = MagicMock()
mock_orchestrator.return_value = MagicMock()
mock_test_case = MagicMock()
mock_test_case.id = 'test-001'
mock_test_case.name = 'Test Case 1'
mock_test_case.module = 'user'
mock_test_case.method.value = 'GET'
mock_test_case.endpoint = '/api/user'
mock_test_case.priority = 'high'
mock_test_case.enabled = True
mock_orchestrator.return_value.list_test_cases.return_value = [mock_test_case]
result = runner.invoke(cli, ['list'])
assert result.exit_code == 0
assert '测试套件' in result.output
assert 'test-001' in result.output
def test_cli_list_with_filter():
"""测试带过滤器的list命令"""
runner = CliRunner()
with patch('apitest.main.ConfigManager') as mock_config:
with patch('apitest.main.LoggerManager') as mock_logger:
with patch('apitest.main.TestOrchestrator') as mock_orchestrator:
mock_config.return_value = MagicMock()
mock_logger.return_value = MagicMock()
mock_orchestrator.return_value = MagicMock()
mock_orchestrator.return_value.list_test_cases.return_value = []
result = runner.invoke(cli, ['list', '--filter', 'priority=high'])
assert result.exit_code == 0
def test_cli_list_with_exception():
"""测试list命令异常处理"""
runner = CliRunner()
with patch('apitest.main.ConfigManager') as mock_config:
mock_config.side_effect = Exception("List error")
result = runner.invoke(cli, ['list'])
assert result.exit_code == 1
assert '列出测试用例时出错' in result.output
def test_cli_report_command():
"""测试report命令"""
runner = CliRunner()
with patch('apitest.main.ConfigManager') as mock_config:
with patch('apitest.main.LoggerManager') as mock_logger:
with patch('apitest.main.ReportManager') as mock_report:
mock_config.return_value = MagicMock()
mock_logger.return_value = MagicMock()
mock_report.return_value = MagicMock()
result = runner.invoke(cli, ['report'])
assert result.exit_code == 0
assert '报告已生成' in result.output
def test_cli_report_with_output():
"""测试带输出路径的report命令"""
runner = CliRunner()
with patch('apitest.main.ConfigManager') as mock_config:
with patch('apitest.main.LoggerManager') as mock_logger:
with patch('apitest.main.ReportManager') as mock_report:
mock_config.return_value = MagicMock()
mock_logger.return_value = MagicMock()
mock_report.return_value = MagicMock()
result = runner.invoke(cli, ['report', '--output', '/tmp/report.html'])
assert result.exit_code == 0
def test_cli_report_with_exception():
"""测试report命令异常处理"""
runner = CliRunner()
with patch('apitest.main.ConfigManager') as mock_config:
mock_config.side_effect = Exception("Report error")
result = runner.invoke(cli, ['report'])
assert result.exit_code == 1
assert '生成报告时出错' in result.output
def test_cli_config_get():
"""测试config get命令"""
runner = CliRunner()
with patch('apitest.main.ConfigManager') as mock_config:
mock_config.return_value = MagicMock()
mock_config.return_value.get.return_value = 'test-value'
result = runner.invoke(cli, ['config', '--get', 'test-key'])
assert result.exit_code == 0
assert 'test-key = test-value' in result.output
def test_cli_config_set():
"""测试config set命令"""
runner = CliRunner()
with patch('apitest.main.ConfigManager') as mock_config:
mock_config.return_value = MagicMock()
result = runner.invoke(cli, ['config', '--set', 'test.key=test-value'])
assert result.exit_code == 0
assert '配置已设置' in result.output
mock_config.return_value.set.assert_called_once_with('test.key', 'test-value')
def test_cli_config_validate():
"""测试config validate命令"""
runner = CliRunner()
with patch('apitest.main.ConfigManager') as mock_config:
mock_config.return_value = MagicMock()
mock_config.return_value.validate.return_value = (True, [])
result = runner.invoke(cli, ['config', '--validate'])
assert result.exit_code == 0
assert '配置验证通过' in result.output
def test_cli_config_validate_with_errors():
"""测试config validate命令带错误"""
runner = CliRunner()
with patch('apitest.main.ConfigManager') as mock_config:
mock_config.return_value = MagicMock()
mock_config.return_value.validate.return_value = (False, ['Error 1', 'Error 2'])
result = runner.invoke(cli, ['config', '--validate'])
assert result.exit_code == 1
assert '配置验证失败' in result.output
def test_cli_config_with_exception():
"""测试config命令异常处理"""
runner = CliRunner()
with patch('apitest.main.ConfigManager') as mock_config:
mock_config.side_effect = Exception("Config error")
result = runner.invoke(cli, ['config'])
assert result.exit_code == 1
assert '配置管理时出错' in result.output
@@ -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