feat(report): auto-generate Chinese HTML report after test session via pytest_sessionfinish hook
Produces output/reports/report_cn.html alongside junit.xml and report.html
This commit is contained in:
+119
-1
@@ -1,4 +1,4 @@
|
|||||||
"""根级 conftest — 全局 fixture:配置加载、日志。
|
"""根级 conftest — 全局 fixture:配置加载、日志、中文 HTML 报告生成。
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
def test_xxx(admin_config): # 管理端配置
|
def test_xxx(admin_config): # 管理端配置
|
||||||
@@ -37,27 +37,145 @@ def admin_config(config):
|
|||||||
"""管理端 (Playwright) 配置。"""
|
"""管理端 (Playwright) 配置。"""
|
||||||
return config["admin"]
|
return config["admin"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def mini_program_config(config):
|
def mini_program_config(config):
|
||||||
"""司机端小程序 (Playwright) 配置。"""
|
"""司机端小程序 (Playwright) 配置。"""
|
||||||
return config["mini_program"]
|
return config["mini_program"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def driver_android_config(config):
|
def driver_android_config(config):
|
||||||
"""司机安卓端 (Appium) 配置。"""
|
"""司机安卓端 (Appium) 配置。"""
|
||||||
return config["driver_android"]
|
return config["driver_android"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def shipper_android_config(config):
|
def shipper_android_config(config):
|
||||||
"""货主安卓端 (Appium) 配置。"""
|
"""货主安卓端 (Appium) 配置。"""
|
||||||
return config["shipper_android"]
|
return config["shipper_android"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def driver_ios_config(config):
|
def driver_ios_config(config):
|
||||||
"""司机苹果端 (Appium) 配置。"""
|
"""司机苹果端 (Appium) 配置。"""
|
||||||
return config["driver_ios"]
|
return config["driver_ios"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def shipper_ios_config(config):
|
def shipper_ios_config(config):
|
||||||
"""货主苹果端 (Appium) 配置。"""
|
"""货主苹果端 (Appium) 配置。"""
|
||||||
return config["shipper_ios"]
|
return config["shipper_ios"]
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# 中文 HTML 报告生成 (pytest_sessionfinish hook)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def pytest_sessionfinish(session):
|
||||||
|
"""测试会话结束后,从 junit.xml 生成一份简洁的中文 HTML 报告。"""
|
||||||
|
from datetime import datetime
|
||||||
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
|
report_dir = Path("output/reports")
|
||||||
|
report_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
junit_path = report_dir / "junit.xml"
|
||||||
|
if not junit_path.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
tree = ET.parse(junit_path)
|
||||||
|
root = tree.getroot()
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
passed = 0
|
||||||
|
failed = 0
|
||||||
|
skipped = 0
|
||||||
|
cases = []
|
||||||
|
|
||||||
|
for suite in root:
|
||||||
|
for case in suite:
|
||||||
|
total += 1
|
||||||
|
name = case.attrib.get("name", "")
|
||||||
|
time_elapsed = case.attrib.get("time", "0")
|
||||||
|
failure = case.find("failure")
|
||||||
|
if failure is not None:
|
||||||
|
failed += 1
|
||||||
|
msg = failure.attrib.get("message", "")[:300]
|
||||||
|
cases.append((name, "失败", time_elapsed, msg))
|
||||||
|
elif case.find("skipped") is not None:
|
||||||
|
skipped += 1
|
||||||
|
cases.append((name, "跳过", time_elapsed, ""))
|
||||||
|
else:
|
||||||
|
passed += 1
|
||||||
|
cases.append((name, "通过", time_elapsed, ""))
|
||||||
|
|
||||||
|
pass_rate = (passed / total * 100) if total > 0 else 0
|
||||||
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
html = _build_cn_report_html(total, passed, failed, skipped, pass_rate, cases, now)
|
||||||
|
(report_dir / "report_cn.html").write_text(html, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _build_cn_report_html(total, passed, failed, skipped, rate, cases, now):
|
||||||
|
"""拼装中文 HTML 报告字符串。"""
|
||||||
|
rows = ""
|
||||||
|
for i, (name, status, elapsed, msg) in enumerate(cases, 1):
|
||||||
|
cls = {"通过": "s-pass", "失败": "s-fail", "跳过": "s-skip"}.get(status, "")
|
||||||
|
msg_html = f'<span class="msg">{msg}</span>' if msg else "-"
|
||||||
|
rows += f"<tr><td>{i}</td><td>{name}</td><td class=\"{cls}\">{status}</td>"
|
||||||
|
rows += f"<td>{float(elapsed):.1f}</td><td>{msg_html}</td></tr>"
|
||||||
|
|
||||||
|
return f"""<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>自动化测试报告 - {now}</title>
|
||||||
|
<style>
|
||||||
|
*{{margin:0;padding:0;box-sizing:border-box}}
|
||||||
|
body{{font-family:'Microsoft YaHei','PingFang SC',sans-serif;background:#f5f7fa;color:#333}}
|
||||||
|
.header{{background:linear-gradient(135deg,#409eff,#337ecc);color:#fff;padding:24px 32px}}
|
||||||
|
.header h1{{font-size:22px;font-weight:500}}
|
||||||
|
.header .time{{font-size:13px;opacity:.75;margin-top:6px}}
|
||||||
|
.summary{{display:flex;gap:20px;padding:24px 32px;background:#fff;border-bottom:1px solid #e4e7ed}}
|
||||||
|
.card{{flex:1;text-align:center;padding:16px;border-radius:8px}}
|
||||||
|
.card .num{{font-size:36px;font-weight:600}}
|
||||||
|
.card .label{{font-size:14px;color:#909399;margin-top:4px}}
|
||||||
|
.card.total{{background:#f0f5ff}}.card.total .num{{color:#409eff}}
|
||||||
|
.card.pass{{background:#f0f9eb}}.card.pass .num{{color:#67c23a}}
|
||||||
|
.card.fail{{background:#fef0f0}}.card.fail .num{{color:#f56c6c}}
|
||||||
|
.card.skip{{background:#fdf6ec}}.card.skip .num{{color:#e6a23c}}
|
||||||
|
.rate{{text-align:center;padding:8px 32px;font-size:16px;color:#67c23a}}
|
||||||
|
.tbl{{margin:20px 32px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow:hidden}}
|
||||||
|
table{{width:100%;border-collapse:collapse}}
|
||||||
|
th{{background:#f5f7fa;text-align:left;padding:12px 16px;font-size:13px;color:#909399;border-bottom:1px solid #ebeef5}}
|
||||||
|
td{{padding:12px 16px;font-size:14px;border-bottom:1px solid #ebeef5}}
|
||||||
|
tr:hover td{{background:#f5f7fa}}
|
||||||
|
.s-pass{{color:#67c23a;font-weight:500}}
|
||||||
|
.s-fail{{color:#f56c6c;font-weight:500}}
|
||||||
|
.s-skip{{color:#e6a23c;font-weight:500}}
|
||||||
|
.msg{{font-size:12px;color:#909399;max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block}}
|
||||||
|
.ft{{text-align:center;padding:20px;font-size:12px;color:#c0c4cc}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>自动化测试报告</h1>
|
||||||
|
<div class="time">执行时间: {now} | 运八网络货运平台 QaAutomationHub</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary">
|
||||||
|
<div class="card total"><div class="num">{total}</div><div class="label">总计</div></div>
|
||||||
|
<div class="card pass"><div class="num">{passed}</div><div class="label">通过</div></div>
|
||||||
|
<div class="card fail"><div class="num">{failed}</div><div class="label">失败</div></div>
|
||||||
|
<div class="card skip"><div class="num">{skipped}</div><div class="label">跳过</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="rate">通过率: {rate:.1f}% ({passed}/{total})</div>
|
||||||
|
<div class="tbl"><table>
|
||||||
|
<thead><tr><th>#</th><th>用例名称</th><th>状态</th><th>耗时(s)</th><th>备注</th></tr></thead>
|
||||||
|
<tbody>{rows}</tbody>
|
||||||
|
</table></div>
|
||||||
|
<div class="ft">报告自动生成于 {now}</div>
|
||||||
|
</body></html>"""
|
||||||
|
|||||||
Reference in New Issue
Block a user