在软件开发过程中,单元测试是保证代码质量的重要手段。而测试报告则是开发团队了解测试覆盖率、发现问题的重要依据。传统的控制台输出测试结果虽然简单直接,但在以下几个方面存在明显不足:
HTML格式的测试报告恰好能解决这些问题。以我们团队的实际经验为例,在引入HTML测试报告后,这些变化非常明显:
Pytest本身不直接支持HTML报告生成,需要安装第三方插件。目前最主流的选择是pytest-html:
bash复制pip install pytest-html
这个插件具有以下特点:
生成HTML报告的最简单命令是:
bash复制pytest --html=report.html
这条命令会在当前目录下生成report.html文件。但实际项目中,我们通常会添加更多参数:
bash复制pytest tests/ --html=reports/report_$(date +%Y%m%d_%H%M%S).html --self-contained-html
参数说明:
tests/:指定测试目录$(date +%Y%m%d_%H%M%S):在报告名中加入时间戳--self-contained-html:生成独立的HTML文件(包含所有CSS/JS)生成的HTML报告主要包含以下几个部分:
概览区域:
结果详情:
额外信息:
pytest-html支持通过hook函数添加自定义内容。在conftest.py中添加以下代码:
python复制def pytest_html_report_title(report):
report.title = "我的项目测试报告"
def pytest_html_results_summary(prefix, summary, postfix):
prefix.extend([html.p("项目版本: 1.0.0")])
prefix.extend([html.p("测试负责人: 张三")])
更强大的自定义方式是通过pytest_metadata钩子添加元数据:
python复制def pytest_configure(config):
config._metadata["项目名称"] = "电商平台"
config._metadata["测试环境"] = "预发布环境"
config._metadata["执行节点"] = "Jenkins构建#123"
对于UI自动化测试,在用例失败时自动截图非常有用。结合selenium可以这样实现:
python复制@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
driver = item.funcargs["driver"] # 假设测试用例中使用了driver fixture
screenshot = driver.get_screenshot_as_base64()
html = f'<div><img src="data:image/png;base64,{screenshot}" alt="screenshot"></div>'
report.extra = [pytest_html.extras.html(html)]
如果默认的样式不符合需求,可以通过CSS进行定制。创建assets/custom.css文件:
css复制/* 修改标题样式 */
h1 {
color: #2c3e50;
border-bottom: 2px solid #3498db;
}
/* 突出显示失败用例 */
tr.failed {
background-color: #ffe6e6;
}
/* 调整表格样式 */
table {
border-collapse: collapse;
width: 100%;
}
然后在conftest.py中指定CSS文件:
python复制def pytest_html_report_title(report):
report._report_css = ["assets/custom.css"]
在Jenkins的构建步骤中这样配置:
bash复制pytest tests/ \
--html=report.html \
--self-contained-html \
--junitxml=report.xml
然后在"Post-build Actions"中添加:
为了避免每次构建覆盖之前的报告,建议使用时间戳命名:
bash复制REPORT_DIR="reports/$(date +%Y-%m-%d)"
mkdir -p ${REPORT_DIR}
pytest --html=${REPORT_DIR}/report_${BUILD_NUMBER}.html
同时可以在Jenkins中配置"Discard Old Builds",只保留最近N次构建的报告。
结合Jenkins的Email Extension插件,可以配置这样的邮件模板:
code复制项目名称: ${PROJECT_NAME}
构建编号: ${BUILD_NUMBER}
测试结果: ${TEST_COUNTS,var="total"}个用例, ${TEST_COUNTS,var="fail"}失败
详细报告: ${BUILD_URL}HTML_20Report
如果在报告中出现中文乱码,需要在conftest.py中添加:
python复制def pytest_html_results_table_header(cells):
cells.insert(1, html.th("描述"))
cells.pop()
def pytest_html_results_table_row(report, cells):
cells.insert(1, html.td(report.description))
cells.pop()
def pytest_html_results_table_html(report, data):
if report.passed:
del data[:]
data.append(html.div("通过", class_="empty log"))
同时在pytest.ini中配置:
ini复制[pytest]
disable_test_id_escaping = true
addopts = --html=report.html --self-contained-html
当测试用例很多时,HTML报告可能会变得很大。解决方案:
bash复制pytest tests/module1 --html=report_module1.html
pytest tests/module2 --html=report_module2.html
使用--html=report.html --self-contained-html参数生成独立文件
在conftest.py中过滤不必要的信息:
python复制def pytest_html_results_table_row(report, cells):
if not hasattr(report, "extra"):
return
report.extra = [e for e in report.extra if not isinstance(e, str)]
pytest-html和allure都是常用的报告工具,主要区别如下:
| 特性 | pytest-html | allure |
|---|---|---|
| 安装复杂度 | 简单 | 中等 |
| 报告美观度 | 一般 | 优秀 |
| 自定义程度 | 高 | 中等 |
| 历史趋势 | 不支持 | 支持 |
| 集成难度 | 低 | 中等 |
| 多语言支持 | 有限 | 完善 |
选择建议:
使用Chart.js可以在报告中添加可视化图表。首先在conftest.py中添加:
python复制def pytest_html_results_summary(prefix, summary, postfix):
prefix.append(html.script(src="https://cdn.jsdelivr.net/npm/chart.js"))
prefix.append(html.canvas(id="myChart"))
chart_js = """
<script>
const ctx = document.getElementById('myChart');
new Chart(ctx, {
type: 'bar',
data: {
labels: ['通过', '失败', '跳过'],
datasets: [{
label: '测试结果统计',
data: [%s, %s, %s],
backgroundColor: [
'rgba(75, 192, 192, 0.2)',
'rgba(255, 99, 132, 0.2)',
'rgba(255, 205, 86, 0.2)'
]
}]
}
});
</script>
""" % (passed, failed, skipped)
prefix.append(html.script(chart_js))
将测试结果存入数据库以便长期分析:
python复制import sqlite3
def pytest_sessionfinish(session, exitstatus):
conn = sqlite3.connect('test_results.db')
c = conn.cursor()
# 创建表(如果不存在)
c.execute('''CREATE TABLE IF NOT EXISTS test_runs
(date text, passed integer, failed integer, skipped integer)''')
# 插入当前运行结果
stats = session.config.pluginmanager.get_plugin('terminalreporter').stats
passed = len(stats.get('passed', []))
failed = len(stats.get('failed', []))
skipped = len(stats.get('skipped', []))
c.execute("INSERT INTO test_runs VALUES (datetime('now'), ?, ?, ?)",
(passed, failed, skipped))
conn.commit()
conn.close()
通过pytest的hook实现报告国际化:
python复制def pytest_html_report_title(report):
report.title = translate("Test Report") # 实现自己的translate函数
def pytest_html_results_table_header(cells):
cells[:] = [translate(cell) for cell in cells]
然后在assets目录下放置不同语言的翻译文件,如zh_CN.json:
json复制{
"Test Report": "测试报告",
"Results": "测试结果",
"Passed": "通过",
"Failed": "失败"
}
当测试用例很多时,报告生成可能变慢。优化方法:
python复制def pytest_runtest_makereport(item, call):
if call.when != "call":
return
# 只收集失败用例的额外信息
if report.failed:
# 收集必要信息
else:
report.extra = []
bash复制pytest -n auto --html=report.html
对于超大规模测试集,可能会遇到内存问题。解决方案:
bash复制pytest tests/partition1 --html=report_part1.html
pytest tests/partition2 --html=report_part2.html
# 然后用脚本合并报告
bash复制pytest -n 4 --dist=loadscope --html=report.html
yaml复制# .github/workflows/tests.yml
jobs:
test:
runs-on: ubuntu-latest
container:
image: python:3.8
options: --memory 4G
测试报告中可能会意外包含敏感信息(密码、密钥等)。防护措施:
python复制def pytest_runtest_makereport(item, call):
if call.when == "call":
for name, value in item.funcargs.items():
if "password" in name or "key" in name:
item.funcargs[name] = "***REDACTED***"
使用环境变量替代硬编码的敏感信息
在CI中配置credentials filtering
生成的HTML报告可能包含内部信息,需要限制访问:
nginx复制location /reports {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
}
结合pytest-cov生成带覆盖率信息的报告:
bash复制pytest --cov=myapp --cov-report=html --html=report.html
然后在conftest.py中添加覆盖率链接:
python复制def pytest_html_results_summary(prefix, summary, postfix):
if os.path.exists("htmlcov/index.html"):
prefix.append(html.a("覆盖率报告", href="htmlcov/index.html"))
在报告中显示测试执行的节点信息:
python复制def pytest_configure(config):
if hasattr(config, "workerinput"):
node = config.workerinput["workerid"]
config._metadata["执行节点"] = node
将测试日志嵌入HTML报告:
python复制def pytest_runtest_makereport(item, call):
if call.when == "call":
logs = caplog.get_records("call")
if logs:
report.extra.append(pytest_html.extras.text("\n".join(logs)))
pytest-html插件更新时,建议:
对于高度定制需求,可以考虑开发专用插件:
python复制class MyHTMLPlugin:
def pytest_html_report_title(self, report):
report.title = "定制报告"
def pytest_html_results_table_header(self, cells):
cells.insert(1, html.th("优先级"))
def pytest_configure(config):
config.pluginmanager.register(MyHTMLPlugin())
建议制定团队内的报告规范:
我们团队采用的规范示例: