Install
openclaw skills install @shylamb-token/smart-test-report从测试执行结果、日志文件或测试框架输出中自动生成专业的测试分析报告。支持 Allure、Pytest、Jest、JUnit 等多种格式。当用户需要生成测试报告、分析测试结果、统计测试通过率、生成质量看板、导出测试数据、或需要测试趋势分析时使用此技能。也适用于用户提到"测试报告"、"test report"、"测试统计"、"测试分析"、"通过率"、"质量报告"、"测试看板"等场景。支持导出为 HTML、PDF、Excel 格式。
openclaw skills install @shylamb-token/smart-test-report你是一个专业的测试数据分析师,帮助用户从测试执行结果中生成可视化、可分析的专业测试报告。
确认用户提供的数据来源:
| 输入类型 | 格式 | 解析方式 |
|---|---|---|
| Pytest 输出 | XML/JSON | 解析 JUnit XML 或 JSON 报告 |
| Allure 结果 | allure-results 目录 | 解析 JSON 结果文件 |
| Jest 输出 | JSON | 解析 --json 输出 |
| JUnit XML | .xml 文件 | 标准 JUnit 格式解析 |
| 手动数据 | Markdown/表格 | 按结构提取数据 |
| 日志文件 | .log 文件 | 正则提取测试结果 |
如果用户没有提供数据文件,主动询问:
根据输入类型选择对应的解析方式:
Pytest JUnit XML 解析:
import xml.etree.ElementTree as ET
def parse_pytest_xml(file_path):
tree = ET.parse(file_path)
root = tree.getroot()
results = {
"total": 0,
"passed": 0,
"failed": 0,
"skipped": 0,
"errors": 0,
"duration": 0,
"test_cases": []
}
for testsuite in root.findall('.//testsuite'):
results["total"] += int(testsuite.get("tests", 0))
results["failed"] += int(testsuite.get("failures", 0))
results["errors"] += int(testsuite.get("errors", 0))
results["skipped"] += int(testsuite.get("skipped", 0))
results["duration"] += float(testsuite.get("time", 0))
for testcase in testsuite.findall('testcase'):
case = {
"name": testcase.get("name"),
"classname": testcase.get("classname"),
"time": float(testcase.get("time", 0)),
"status": "passed"
}
if testcase.find('failure') is not None:
case["status"] = "failed"
case["error_message"] = testcase.find('failure').get("message")
elif testcase.find('skipped') is not None:
case["status"] = "skipped"
results["test_cases"].append(case)
results["passed"] = results["total"] - results["failed"] - results["skipped"] - results["errors"]
return results
| 指标 | 计算方式 | 说明 |
|---|---|---|
| 通过率 | passed / total × 100% | 核心质量指标 |
| 执行效率 | total / duration | 每秒执行的用例数 |
| 失败集中度 | 失败模块/总模块 | 定位问题集中区域 |
| 平均执行时间 | duration / total | 单用例平均耗时 |
| 回归率 | 新增失败 / 上次通过 | 质量回归程度 |
标准报告包含以下章节:
概览仪表盘
失败分析
性能分析
覆盖率分析(如有数据)
趋势分析(如有历史数据)
# 测试报告
**执行时间**:2024-01-15 10:30:00
**总耗时**:125.6s
**环境**:Python 3.10 / Chrome 120
## 📊 概览
| 指标 | 数值 | 趋势 |
|------|------|------|
| 总用例 | 256 | ↑ +12 |
| 通过 | 241 | ↑ +8 |
| 失败 | 12 | ↓ -2 |
| 跳过 | 3 | → |
| **通过率** | **94.1%** | ↑ +2.3% |
## ❌ 失败用例分析
### 按模块分布
| 模块 | 失败数 | 占比 |
|------|--------|------|
| 登录模块 | 5 | 41.7% |
| 支付模块 | 4 | 33.3% |
| 用户模块 | 3 | 25.0% |
### 失败详情
| # | 用例 | 错误类型 | 错误信息 |
|---|------|---------|---------|
| 1 | test_login_timeout | Timeout | 页面加载超时 |
| 2 | test_payment_verify | AssertionError | 金额不匹配 |
## ⏱️ 性能 Top 5
| 用例 | 耗时 | 状态 |
|------|------|------|
| test_import_data | 15.2s | ✅ |
| test_export_report | 12.8s | ✅ |
| test_batch_delete | 8.5s | ❌ |
## 📈 建议
1. 登录模块失败率较高,建议优先排查
2. test_import_data 执行时间过长,建议优化
当用户需要 HTML 报告时,生成以下代码:
import json
from datetime import datetime
def generate_html_report(data, output_path):
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试报告 - {datetime.now().strftime('%Y-%m-%d')}</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {{ font-family: -apple-system, sans-serif; margin: 40px; background: #f5f5f5; }}
.container {{ max-width: 1200px; margin: 0 auto; }}
.header {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 12px; margin-bottom: 20px; }}
.card {{ background: white; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }}
.metrics {{ display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }}
.metric {{ text-align: center; padding: 20px; background: #f8f9fa; border-radius: 8px; }}
.metric-value {{ font-size: 32px; font-weight: bold; color: #333; }}
.metric-label {{ color: #666; margin-top: 8px; }}
.pass {{ color: #22c55e; }}
.fail {{ color: #ef4444; }}
table {{ width: 100%; border-collapse: collapse; }}
th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #eee; }}
th {{ background: #f8f9fa; font-weight: 600; }}
.chart-container {{ height: 300px; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📋 测试报告</h1>
<p>生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
</div>
<div class="card">
<h2>📊 测试概览</h2>
<div class="metrics">
<div class="metric">
<div class="metric-value">{data['total']}</div>
<div class="metric-label">总用例</div>
</div>
<div class="metric">
<div class="metric-value pass">{data['passed']}</div>
<div class="metric-label">通过</div>
</div>
<div class="metric">
<div class="metric-value fail">{data['failed']}</div>
<div class="metric-label">失败</div>
</div>
<div class="metric">
<div class="metric-value">{data['pass_rate']:.1f}%</div>
<div class="metric-label">通过率</div>
</div>
</div>
</div>
<div class="card">
<h2>📈 结果分布</h2>
<div class="chart-container">
<canvas id="pieChart"></canvas>
</div>
</div>
<div class="card">
<h2>❌ 失败用例详情</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>用例名称</th>
<th>模块</th>
<th>错误类型</th>
<th>错误信息</th>
<th>严重程度</th>
</tr>
</thead>
<tbody>
{''.join(f'<tr><td>{i+1}</td><td>{c["name"]}</td><td>{c.get("module","-")}</td><td>{c.get("error_type","-")}</td><td>{c.get("error_message","-")}</td><td>{c.get("severity","-")}</td></tr>' for i, c in enumerate(data.get('failed_cases', [])))}
</tbody>
</table>
</div>
<div class="card">
<h2>⏱️ 性能分析 - 最慢 Top 10</h2>
<div class="chart-container">
<canvas id="barChart"></canvas>
</div>
<table>
<thead>
<tr>
<th>#</th>
<th>用例名称</th>
<th>耗时(s)</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{''.join(f'<tr><td>{i+1}</td><td>{c["name"]}</td><td>{c["time"]:.2f}</td><td>{"✅" if c["status"]=="passed" else "❌"}</td></tr>' for i, c in enumerate(data.get('slowest_cases', [])[:10]))}
</tbody>
</table>
</div>
<div class="card">
<h2>🔍 失败模式分析</h2>
<table>
<thead>
<tr>
<th>失败模式</th>
<th>出现次数</th>
<th>占比</th>
<th>修复建议</th>
</tr>
</thead>
<tbody>
{''.join(f'<tr><td>{p["pattern"]}</td><td>{p["count"]}</td><td>{p["percentage"]:.1f}%</td><td>{p["suggestion"]}</td></tr>' for p in data.get('failure_patterns', []))}
</tbody>
</table>
</div>
<div class="card">
<h2>💡 修复建议</h2>
<ol>
{''.join(f'<li>{s}</li>' for s in data.get('suggestions', []))}
</ol>
</div>
<div class="card">
<h2>📋 发布结论</h2>
<p style="font-size:18px;font-weight:bold;color:{data.get('release_color','#333')}">{data.get('release_verdict','待定')}</p>
<p>{data.get('release_note','')}</p>
</div>
</div>
<script>
const ctx = document.getElementById('pieChart').getContext('2d');
new Chart(ctx, {{
type: 'doughnut',
data: {{
labels: ['通过', '失败', '跳过'],
datasets: [{{
data: [{data['passed']}, {data['failed']}, {data['skipped']}],
backgroundColor: ['#22c55e', '#ef4444', '#94a3b8']
}}]
}},
options: {{ responsive: true, plugins: {{ legend: {{ position: 'bottom' }} }} }}
}});
const ctx2 = document.getElementById('barChart').getContext('2d');
new Chart(ctx2, {{
type: 'bar',
data: {{
labels: {json.dumps([c['name'] for c in data.get('slowest_cases', [])[:10]], ensure_ascii=False)},
datasets: [{{
label: '耗时(秒)',
data: {json.dumps([round(c['time'], 2) for c in data.get('slowest_cases', [])[:10]])},
backgroundColor: '#6366f1'
}}]
}},
options: {{ responsive: true, indexAxis: 'y', plugins: {{ legend: {{ display: false }} }} }}
}});
</script>
</body>
</html>
"""
with open(output_path, 'w', encoding='utf-8') as f:
f.write(html)
return output_path
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.chart import PieChart, Reference
def generate_excel_report(data, output_path):
wb = openpyxl.Workbook()
# 概览页
ws = wb.active
ws.title = "测试概览"
# 标题样式
title_font = Font(size=16, bold=True)
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF")
# 写入概览数据
ws['A1'] = "测试报告"
ws['A1'].font = title_font
ws['A3'] = "指标"
ws['B3'] = "数值"
for cell in ws[3]:
cell.font = header_font
cell.fill = header_fill
metrics = [
("总用例数", data['total']),
("通过", data['passed']),
("失败", data['failed']),
("跳过", data['skipped']),
("通过率", f"{data['pass_rate']:.1f}%"),
("执行时间", f"{data['duration']:.1f}s")
]
for i, (label, value) in enumerate(metrics, start=4):
ws[f'A{i}'] = label
ws[f'B{i}'] = value
# 饼图
pie = PieChart()
pie.title = "测试结果分布"
labels = Reference(ws, min_col=1, min_row=4, max_row=6)
values = Reference(ws, min_col=2, min_row=4, max_row=6)
pie.add_data(values)
pie.set_categories(labels)
ws.add_chart(pie, "D3")
# 失败用例详情页
if data.get('failed_cases'):
ws2 = wb.create_sheet("失败用例")
ws2.append(["用例ID", "用例名称", "模块", "错误类型", "错误信息"])
for cell in ws2[1]:
cell.font = header_font
cell.fill = header_fill
for case in data['failed_cases']:
ws2.append([case['id'], case['name'], case['module'], case['error_type'], case['message']])
wb.save(output_path)
return output_path
如果用户提供了多轮测试数据,生成趋势分析:
def analyze_trend(history_data):
"""分析测试趋势"""
trend = {
"pass_rate_trend": [], # 通过率变化
"total_trend": [], # 用例数变化
"duration_trend": [], # 执行时间变化
"quality_score": 0 # 质量评分
}
for run in history_data:
trend["pass_rate_trend"].append(run["pass_rate"])
trend["total_trend"].append(run["total"])
trend["duration_trend"].append(run["duration"])
# 计算质量评分(通过率权重 60%,稳定性 40%)
avg_pass_rate = sum(trend["pass_rate_trend"]) / len(trend["pass_rate_trend"])
stability = 100 - (max(trend["pass_rate_trend"]) - min(trend["pass_rate_trend"]))
trend["quality_score"] = avg_pass_rate * 0.6 + stability * 0.4
return trend
自动归类失败原因:
| 失败模式 | 识别特征 | 建议 |
|---|---|---|
| 超时失败 | "timeout"、"超时" | 增加等待时间或优化性能 |
| 断言失败 | "assert"、"expected" | 检查业务逻辑或测试数据 |
| 元素定位 | "not found"、"unable to locate" | 更新选择器或检查页面结构 |
| 网络错误 | "connection"、"network"、"503" | 检查网络或服务状态 |
| 数据问题 | "null"、"undefined"、"empty" | 检查测试数据准备 |