Files
QaAutomationHub/tests/conftest.py
T
xst 273f4f0225 fix(report): extract Chinese test name from docstring metadata field
Priority: 用例名称: xxx > display_name > docstring first line
2026-07-23 16:57:30 +08:00

232 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""根级 conftest — 全局 fixture:配置加载、日志、中文 HTML 报告生成。
Usage:
def test_xxx(admin_config): # 管理端配置
def test_xxx(driver_android_config): # 司机安卓配置
"""
from pathlib import Path
import sys
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent))
@pytest.fixture(scope="session")
def config():
"""全局配置(session 级,只加载一次)。"""
from tests.config.test_config import load_config
return load_config()
@pytest.fixture(scope="function")
def logger(request):
"""每个测试函数的 logger。"""
from tests.utils.logger import get_logger
module_name = request.module.__name__.replace("tests.", "")
return get_logger(module_name)
# ═══════════════════════════════════════════════════════════════════
# 各端配置 fixture(从 config 中摘取对应 section
# ═══════════════════════════════════════════════════════════════════
@pytest.fixture(scope="session")
def admin_config(config):
"""管理端 (Playwright) 配置。"""
return config["admin"]
@pytest.fixture(scope="session")
def mini_program_config(config):
"""司机端小程序 (Playwright) 配置。"""
return config["mini_program"]
@pytest.fixture(scope="session")
def driver_android_config(config):
"""司机安卓端 (Appium) 配置。"""
return config["driver_android"]
@pytest.fixture(scope="session")
def shipper_android_config(config):
"""货主安卓端 (Appium) 配置。"""
return config["shipper_android"]
@pytest.fixture(scope="session")
def driver_ios_config(config):
"""司机苹果端 (Appium) 配置。"""
return config["driver_ios"]
@pytest.fixture(scope="session")
def shipper_ios_config(config):
"""货主苹果端 (Appium) 配置。"""
return config["shipper_ios"]
# ═══════════════════════════════════════════════════════════════════
# 中文 HTML 报告生成 (pytest_sessionfinish hook)
# ═══════════════════════════════════════════════════════════════════
def pytest_sessionfinish(session):
"""测试会话结束后,生成中文 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
func_name = case.attrib.get("name", "")
cn_name = _resolve_cn_name(session, func_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((cn_name, "失败", time_elapsed, msg))
elif case.find("skipped") is not None:
skipped += 1
cases.append((cn_name, "跳过", time_elapsed, ""))
else:
passed += 1
cases.append((cn_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-sig")
def _resolve_cn_name(session, func_name: str) -> str:
"""从用例元信息中提取中文名称。
优先级:
1) 用例 docstring 中显式声明的 "用例名称: xxx"
2) display_name 属性
3) docstring 第一行去 【P0】 前缀
用例 docstring 格式示例:
用例ID: TC-D2S-001
用例名称: 司机APP接单到管理端核算打款完整流程
优先级: P0
"""
for item in session.items:
item_func_name = getattr(item, "originalname", None) or item.name.rsplit("::", 1)[-1]
if item_func_name == func_name:
test_cls = item.cls if hasattr(item, "cls") else None
test_func = getattr(test_cls, item_func_name, None) if test_cls else None
doc = (test_func.__doc__ or "") if test_func else ""
# 策略1: 从 docstring 中提取 "用例名称: xxx"
import re
m = re.search(r"用例名称[:]\s*(.+)", doc)
if m:
return m.group(1).strip()
# 策略2: display_name 属性
if test_func and hasattr(test_func, "display_name"):
return test_func.display_name
# 策略3: docstring 第一行去 【P0】 前缀
if doc:
first_line = doc.strip().split("\n")[0].strip()
if first_line.startswith("【"):
idx = first_line.find("】")
if idx > 0:
first_line = first_line[idx + 1:]
if first_line and len(first_line) > 2:
return first_line
break
return func_name
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-sig")
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>"""