feat: 添加测试框架和覆盖率报告功能
feat(测试): 新增Playwright和Vitest测试配置 feat(测试): 添加测试覆盖率报告生成功能 feat(测试): 实现前后端测试脚本集成 fix(测试): 修复测试密码不匹配问题 fix(测试): 修正URL等待策略 fix(测试): 调整错误消息选择器 refactor(测试): 重构测试目录结构 refactor(测试): 优化测试用例组织方式 docs: 更新测试报告文档 docs: 添加测试覆盖率报告模板 ci: 添加Docker测试环境配置 ci: 实现测试自动化脚本 chore: 更新依赖版本 chore: 添加测试相关配置文件
This commit is contained in:
@@ -0,0 +1,734 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试报告生成脚本
|
||||
生成详细的HTML测试报告,包含测试结果、截图、缺陷统计等
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional
|
||||
from dataclasses import dataclass, asdict
|
||||
import base64
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestCase:
|
||||
"""测试用例"""
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
status: str # passed, failed, skipped
|
||||
duration: float
|
||||
error_message: Optional[str] = None
|
||||
screenshot_path: Optional[str] = None
|
||||
steps: List[str] = None
|
||||
timestamp: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.steps is None:
|
||||
self.steps = []
|
||||
self.timestamp = datetime.now().isoformat()
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestSuite:
|
||||
"""测试套件"""
|
||||
name: str
|
||||
test_cases: List[TestCase]
|
||||
start_time: str
|
||||
end_time: str
|
||||
total_duration: float
|
||||
|
||||
@property
|
||||
def total_tests(self) -> int:
|
||||
return len(self.test_cases)
|
||||
|
||||
@property
|
||||
def passed_tests(self) -> int:
|
||||
return len([tc for tc in self.test_cases if tc.status == "passed"])
|
||||
|
||||
@property
|
||||
def failed_tests(self) -> int:
|
||||
return len([tc for tc in self.test_cases if tc.status == "failed"])
|
||||
|
||||
@property
|
||||
def skipped_tests(self) -> int:
|
||||
return len([tc for tc in self.test_cases if tc.status == "skipped"])
|
||||
|
||||
@property
|
||||
def pass_rate(self) -> float:
|
||||
if self.total_tests == 0:
|
||||
return 0.0
|
||||
return (self.passed_tests / self.total_tests) * 100
|
||||
|
||||
|
||||
class TestReportGenerator:
|
||||
"""测试报告生成器"""
|
||||
|
||||
def __init__(self, output_dir: str = "test_reports"):
|
||||
self.output_dir = Path(output_dir)
|
||||
self.output_dir.mkdir(exist_ok=True)
|
||||
self.test_suites: List[TestSuite] = []
|
||||
|
||||
def add_test_suite(self, test_suite: TestSuite):
|
||||
"""添加测试套件"""
|
||||
self.test_suites.append(test_suite)
|
||||
|
||||
def generate_html_report(self) -> str:
|
||||
"""生成HTML报告"""
|
||||
html_content = self._generate_html()
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
report_file = self.output_dir / f"test_report_{timestamp}.html"
|
||||
|
||||
with open(report_file, 'w', encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
|
||||
return str(report_file)
|
||||
|
||||
def generate_json_report(self) -> str:
|
||||
"""生成JSON报告"""
|
||||
report_data = {
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"test_suites": [asdict(suite) for suite in self.test_suites],
|
||||
"summary": self._generate_summary()
|
||||
}
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
report_file = self.output_dir / f"test_report_{timestamp}.json"
|
||||
|
||||
with open(report_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(report_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
return str(report_file)
|
||||
|
||||
def _generate_summary(self) -> Dict:
|
||||
"""生成测试摘要"""
|
||||
total_tests = sum(suite.total_tests for suite in self.test_suites)
|
||||
total_passed = sum(suite.passed_tests for suite in self.test_suites)
|
||||
total_failed = sum(suite.failed_tests for suite in self.test_suites)
|
||||
total_skipped = sum(suite.skipped_tests for suite in self.test_suites)
|
||||
total_duration = sum(suite.total_duration for suite in self.test_suites)
|
||||
|
||||
overall_pass_rate = (total_passed / total_tests * 100) if total_tests > 0 else 0
|
||||
|
||||
# 统计缺陷
|
||||
defects = []
|
||||
for suite in self.test_suites:
|
||||
for test_case in suite.test_cases:
|
||||
if test_case.status == "failed" and test_case.error_message:
|
||||
defects.append({
|
||||
"test_case": test_case.name,
|
||||
"test_suite": suite.name,
|
||||
"error": test_case.error_message,
|
||||
"timestamp": test_case.timestamp
|
||||
})
|
||||
|
||||
return {
|
||||
"total_tests": total_tests,
|
||||
"total_passed": total_passed,
|
||||
"total_failed": total_failed,
|
||||
"total_skipped": total_skipped,
|
||||
"overall_pass_rate": overall_pass_rate,
|
||||
"total_duration": total_duration,
|
||||
"defects": defects,
|
||||
"test_suites_count": len(self.test_suites)
|
||||
}
|
||||
|
||||
def _generate_html(self) -> str:
|
||||
"""生成HTML内容"""
|
||||
summary = self._generate_summary()
|
||||
|
||||
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>E2E和UAT测试报告</title>
|
||||
<style>
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
|
||||
body {{
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
}}
|
||||
|
||||
.container {{
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
overflow: hidden;
|
||||
}}
|
||||
|
||||
.header {{
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}}
|
||||
|
||||
.header h1 {{
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}}
|
||||
|
||||
.header p {{
|
||||
font-size: 1.1em;
|
||||
opacity: 0.9;
|
||||
}}
|
||||
|
||||
.summary {{
|
||||
padding: 30px;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}}
|
||||
|
||||
.summary-grid {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}}
|
||||
|
||||
.summary-card {{
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
}}
|
||||
|
||||
.summary-card h3 {{
|
||||
font-size: 2em;
|
||||
margin-bottom: 5px;
|
||||
}}
|
||||
|
||||
.summary-card p {{
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}}
|
||||
|
||||
.summary-card.total h3 {{ color: #667eea; }}
|
||||
.summary-card.passed h3 {{ color: #28a745; }}
|
||||
.summary-card.failed h3 {{ color: #dc3545; }}
|
||||
.summary-card.rate h3 {{ color: #17a2b8; }}
|
||||
|
||||
.test-suites {{
|
||||
padding: 30px;
|
||||
}}
|
||||
|
||||
.test-suite {{
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}}
|
||||
|
||||
.test-suite-header {{
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}}
|
||||
|
||||
.test-suite-header h2 {{
|
||||
margin: 0;
|
||||
color: #667eea;
|
||||
}}
|
||||
|
||||
.test-suite-stats {{
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}}
|
||||
|
||||
.stat-badge {{
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
}}
|
||||
|
||||
.stat-badge.total {{ background: #667eea; color: white; }}
|
||||
.stat-badge.passed {{ background: #28a745; color: white; }}
|
||||
.stat-badge.failed {{ background: #dc3545; color: white; }}
|
||||
.stat-badge.duration {{ background: #17a2b8; color: white; }}
|
||||
|
||||
.test-cases {{
|
||||
padding: 20px;
|
||||
}}
|
||||
|
||||
.test-case {{
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 15px;
|
||||
overflow: hidden;
|
||||
}}
|
||||
|
||||
.test-case-header {{
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}}
|
||||
|
||||
.test-case-header:hover {{
|
||||
background: #f8f9fa;
|
||||
}}
|
||||
|
||||
.test-case-info {{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}}
|
||||
|
||||
.test-case-id {{
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}}
|
||||
|
||||
.test-case-name {{
|
||||
font-weight: 500;
|
||||
}}
|
||||
|
||||
.test-case-status {{
|
||||
padding: 5px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}}
|
||||
|
||||
.test-case-status.passed {{ background: #d4edda; color: #155724; }}
|
||||
.test-case-status.failed {{ background: #f8d7da; color: #721c24; }}
|
||||
.test-case-status.skipped {{ background: #fff3cd; color: #856404; }}
|
||||
|
||||
.test-case-details {{
|
||||
padding: 15px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
display: none;
|
||||
}}
|
||||
|
||||
.test-case-details.show {{
|
||||
display: block;
|
||||
}}
|
||||
|
||||
.test-case-description {{
|
||||
margin-bottom: 15px;
|
||||
color: #666;
|
||||
}}
|
||||
|
||||
.test-case-steps {{
|
||||
margin-bottom: 15px;
|
||||
}}
|
||||
|
||||
.test-case-steps h4 {{
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}}
|
||||
|
||||
.test-case-steps ul {{
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}}
|
||||
|
||||
.test-case-steps li {{
|
||||
padding: 5px 0;
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
}}
|
||||
|
||||
.test-case-steps li:before {{
|
||||
content: "✓";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
}}
|
||||
|
||||
.test-case-error {{
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
}}
|
||||
|
||||
.test-case-error h4 {{
|
||||
color: #721c24;
|
||||
margin-bottom: 10px;
|
||||
}}
|
||||
|
||||
.test-case-error pre {{
|
||||
background: #f5c6cb;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.9em;
|
||||
}}
|
||||
|
||||
.test-case-screenshot {{
|
||||
margin-top: 15px;
|
||||
}}
|
||||
|
||||
.test-case-screenshot img {{
|
||||
max-width: 100%;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}}
|
||||
|
||||
.test-case-meta {{
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
margin-top: 10px;
|
||||
}}
|
||||
|
||||
.defects-section {{
|
||||
padding: 30px;
|
||||
background: #fff3cd;
|
||||
border-top: 1px solid #ffeeba;
|
||||
}}
|
||||
|
||||
.defects-section h2 {{
|
||||
color: #856404;
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
|
||||
.defect-item {{
|
||||
background: white;
|
||||
border: 1px solid #ffeeba;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
}}
|
||||
|
||||
.defect-item h3 {{
|
||||
color: #856404;
|
||||
margin-bottom: 10px;
|
||||
}}
|
||||
|
||||
.defect-item p {{
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}}
|
||||
|
||||
.footer {{
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background: #f8f9fa;
|
||||
color: #666;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}}
|
||||
|
||||
@media (max-width: 768px) {{
|
||||
.summary-grid {{
|
||||
grid-template-columns: 1fr;
|
||||
}}
|
||||
|
||||
.test-suite-header {{
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}}
|
||||
|
||||
.test-case-header {{
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🧪 E2E和UAT测试报告</h1>
|
||||
<p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||
</div>
|
||||
|
||||
<div class="summary">
|
||||
<h2>📊 测试摘要</h2>
|
||||
<div class="summary-grid">
|
||||
<div class="summary-card total">
|
||||
<h3>{summary['total_tests']}</h3>
|
||||
<p>总测试数</p>
|
||||
</div>
|
||||
<div class="summary-card passed">
|
||||
<h3>{summary['total_passed']}</h3>
|
||||
<p>通过测试</p>
|
||||
</div>
|
||||
<div class="summary-card failed">
|
||||
<h3>{summary['total_failed']}</h3>
|
||||
<p>失败测试</p>
|
||||
</div>
|
||||
<div class="summary-card rate">
|
||||
<h3>{summary['overall_pass_rate']:.1f}%</h3>
|
||||
<p>通过率</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-suites">
|
||||
<h2>📋 测试套件</h2>
|
||||
"""
|
||||
|
||||
# 生成测试套件内容
|
||||
for suite in self.test_suites:
|
||||
html += f"""
|
||||
<div class="test-suite">
|
||||
<div class="test-suite-header">
|
||||
<h2>{suite.name}</h2>
|
||||
<div class="test-suite-stats">
|
||||
<span class="stat-badge total">总计: {suite.total_tests}</span>
|
||||
<span class="stat-badge passed">通过: {suite.passed_tests}</span>
|
||||
<span class="stat-badge failed">失败: {suite.failed_tests}</span>
|
||||
<span class="stat-badge duration">耗时: {suite.total_duration:.2f}s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="test-cases">
|
||||
"""
|
||||
|
||||
# 生成测试用例
|
||||
for test_case in suite.test_cases:
|
||||
status_class = test_case.status
|
||||
status_text = "通过" if test_case.status == "passed" else "失败" if test_case.status == "failed" else "跳过"
|
||||
|
||||
html += f"""
|
||||
<div class="test-case">
|
||||
<div class="test-case-header" onclick="toggleDetails('{test_case.id}')">
|
||||
<div class="test-case-info">
|
||||
<span class="test-case-id">{test_case.id}</span>
|
||||
<span class="test-case-name">{test_case.name}</span>
|
||||
</div>
|
||||
<span class="test-case-status {status_class}">{status_text}</span>
|
||||
</div>
|
||||
<div class="test-case-details" id="details-{test_case.id}">
|
||||
<p class="test-case-description">{test_case.description}</p>
|
||||
|
||||
<div class="test-case-steps">
|
||||
<h4>测试步骤:</h4>
|
||||
<ul>
|
||||
"""
|
||||
|
||||
for step in test_case.steps:
|
||||
html += f" <li>{step}</li>\n"
|
||||
|
||||
html += """
|
||||
</ul>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# 错误信息
|
||||
if test_case.error_message:
|
||||
html += f"""
|
||||
<div class="test-case-error">
|
||||
<h4>❌ 错误信息:</h4>
|
||||
<pre>{test_case.error_message}</pre>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# 截图
|
||||
if test_case.screenshot_path and Path(test_case.screenshot_path).exists():
|
||||
try:
|
||||
with open(test_case.screenshot_path, 'rb') as img_file:
|
||||
img_data = base64.b64encode(img_file.read()).decode()
|
||||
html += f"""
|
||||
<div class="test-case-screenshot">
|
||||
<h4>📸 失败截图:</h4>
|
||||
<img src="data:image/png;base64,{img_data}" alt="测试失败截图" onclick="window.open(this.src)">
|
||||
</div>
|
||||
"""
|
||||
except Exception as e:
|
||||
html += f"""
|
||||
<div class="test-case-screenshot">
|
||||
<h4>📸 失败截图:</h4>
|
||||
<p>截图加载失败: {str(e)}</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html += f"""
|
||||
<div class="test-case-meta">
|
||||
<span>⏱️ 耗时: {test_case.duration:.2f}s</span>
|
||||
<span>🕐 时间: {test_case.timestamp}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html += """
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# 缺陷部分
|
||||
if summary['defects']:
|
||||
html += """
|
||||
</div>
|
||||
|
||||
<div class="defects-section">
|
||||
<h2>🐛 缺陷统计</h2>
|
||||
"""
|
||||
for defect in summary['defects']:
|
||||
html += f"""
|
||||
<div class="defect-item">
|
||||
<h3>❌ {defect['test_case']}</h3>
|
||||
<p><strong>测试套件:</strong> {defect['test_suite']}</p>
|
||||
<p><strong>错误:</strong> {defect['error']}</p>
|
||||
<p><strong>时间:</strong> {defect['timestamp']}</p>
|
||||
</div>
|
||||
"""
|
||||
html += """
|
||||
</div>
|
||||
"""
|
||||
|
||||
html += """
|
||||
<div class="footer">
|
||||
<p>🤖 自动化测试系统 | Novalon Manage System</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleDetails(testCaseId) {
|
||||
const details = document.getElementById('details-' + testCaseId);
|
||||
details.classList.toggle('show');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return html
|
||||
|
||||
|
||||
def create_sample_report():
|
||||
"""创建示例报告"""
|
||||
generator = TestReportGenerator()
|
||||
|
||||
# 创建测试用例
|
||||
test_cases = [
|
||||
TestCase(
|
||||
id="TC-001",
|
||||
name="完整登录流程",
|
||||
description="测试管理员和普通用户的完整登录流程,包括UI登录和API验证",
|
||||
status="passed",
|
||||
duration=12.5,
|
||||
steps=[
|
||||
"打开登录页面",
|
||||
"输入管理员用户名和密码",
|
||||
"点击登录按钮",
|
||||
"验证跳转到Dashboard页面",
|
||||
"验证Dashboard数据加载",
|
||||
"通过API验证登录日志",
|
||||
"测试普通用户登录"
|
||||
]
|
||||
),
|
||||
TestCase(
|
||||
id="TC-002",
|
||||
name="角色管理完整流程",
|
||||
description="测试角色管理的完整流程,包括创建、编辑、删除角色",
|
||||
status="passed",
|
||||
duration=8.3,
|
||||
steps=[
|
||||
"登录系统",
|
||||
"导航到角色管理页面",
|
||||
"验证角色列表显示",
|
||||
"创建新角色",
|
||||
"编辑角色信息",
|
||||
"删除角色"
|
||||
]
|
||||
),
|
||||
TestCase(
|
||||
id="TC-003",
|
||||
name="菜单管理数据验证",
|
||||
description="验证菜单管理页面的数据结构和字段映射",
|
||||
status="passed",
|
||||
duration=6.2,
|
||||
steps=[
|
||||
"登录系统",
|
||||
"导航到菜单管理页面",
|
||||
"验证菜单树结构",
|
||||
"验证一级菜单显示",
|
||||
"验证二级菜单显示",
|
||||
"通过API验证字段映射"
|
||||
]
|
||||
),
|
||||
TestCase(
|
||||
id="TC-004",
|
||||
name="前后端字段映射一致性",
|
||||
description="验证前后端API的字段映射一致性",
|
||||
status="passed",
|
||||
duration=4.8,
|
||||
steps=[
|
||||
"登录系统",
|
||||
"验证角色API字段映射",
|
||||
"验证菜单API字段映射",
|
||||
"验证用户API字段映射"
|
||||
]
|
||||
),
|
||||
TestCase(
|
||||
id="TC-005",
|
||||
name="RBAC权限验证",
|
||||
description="验证基于角色的访问控制权限",
|
||||
status="passed",
|
||||
duration=7.1,
|
||||
steps=[
|
||||
"测试管理员权限",
|
||||
"验证管理员可访问所有菜单",
|
||||
"测试普通用户权限",
|
||||
"验证普通用户只能看到授权菜单",
|
||||
"测试未授权访问拦截"
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
# 创建测试套件
|
||||
test_suite = TestSuite(
|
||||
name="E2E和UAT测试套件",
|
||||
test_cases=test_cases,
|
||||
start_time=datetime.now().isoformat(),
|
||||
end_time=datetime.now().isoformat(),
|
||||
total_duration=sum(tc.duration for tc in test_cases)
|
||||
)
|
||||
|
||||
generator.add_test_suite(test_suite)
|
||||
|
||||
# 生成报告
|
||||
html_report = generator.generate_html_report()
|
||||
json_report = generator.generate_json_report()
|
||||
|
||||
print(f"✅ HTML报告已生成: {html_report}")
|
||||
print(f"✅ JSON报告已生成: {json_report}")
|
||||
|
||||
return generator
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="测试报告生成器")
|
||||
parser.add_argument("--output-dir", default="test_reports", help="报告输出目录")
|
||||
parser.add_argument("--sample", action="store_true", help="生成示例报告")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.sample:
|
||||
generator = create_sample_report()
|
||||
else:
|
||||
print("请提供测试数据或使用 --sample 生成示例报告")
|
||||
print("示例: python test_report_generator.py --sample")
|
||||
Reference in New Issue
Block a user