生成测试报告
测试报告中至少可以显示执行了多少条用例,用例信息如何,多少条用例执行成功,多少条用例执行失败,多少用例出错
使用步骤
1)安装pytest-html插件
pip install pytest-html
2)导入pytest
import pytest
3)运行测试生成测试报告
代码中运行
pytest.main([‘-v’, ‘--tb=line’, ‘--html=报告名.html’, ‘--self-contained-html’, ‘测试模块名.py’])
--self-contained-html:表示生成独立的测试报告,与css样式等无关,方便拷贝,如果不加此项,报告拷贝到他处后,只有文本,没有格式,不美观
终端中运行
pytest --html=报告名.html --self-contained-html 模块名.py
直接html独立显示,否则受css影响
添加self-contained-html后,只拷贝html文件即可分享
--html参数也可用于pytest.main()中
报告简易程度
pytest --tb=short --html=报告名.html --self-contained-html 模块名.py
--tb=short
断言失败时,显示
断言所在模块中函数内的行号、断言语句、断言自定义消息
预期结果(前缀-标识)
实际结果(前缀+标识)
优化测试报告
设置报告字体、颜色、表格边框等
1)覆盖python\Lib\site-packages\pytest_html\plugin.py
或
C:\Users\tedu\AppData\Roaming\Python\Python38\site-packages\pytest_html\plugin.py
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import bisect
import datetime
import importlib
import json
import os
import re
import time
import warnings
from base64 import b64decode
from base64 import b64encode
from collections import defaultdict
from collections import OrderedDict
from functools import lru_cache
from html import escape
from os.path import isfile
import pkg_resources
import pytest
from _pytest.logging import _remove_ansi_escape_sequences
from py.xml import html
from py.xml import raw
from . import __pypi_url__
from . import __version__
from . import extras
@lru_cache()
def ansi_support():
try:
# from ansi2html import Ansi2HTMLConverter, style # NOQA
return importlib.import_module("ansi2html")
except ImportError:
# ansi2html is not installed
pass
def pytest_addhooks(pluginmanager):
from . import hooks
pluginmanager.add_hookspecs(hooks)
def pytest_addoption(parser):
group = parser.getgroup("terminal reporting")
group.addoption(
"--html",
action="store",
dest="htmlpath",
metavar="path",
default=None,
help="create html report file at given path.",
)
group.addoption(
"--self-contained-html",
action="store_true",
help="create a self-contained html file containing all "
"necessary styles, scripts, and images - this means "
"that the report may not render or function where CSP "
"restrictions are in place (see "
"https://developer.mozilla.org/docs/Web/Security/CSP)",
)
group.addoption(
"--css",
action="append",
metavar="path",
default=[],
help="append given css file content to report style file.",
)
parser.addini(
"render_collapsed",
type="bool",
default=False,
help="Open the report with all rows collapsed. Useful for very large reports",
)
parser.addini(
"max_asset_filename_length",
default=255,
help="set the maximum filename length for assets "
"attached to the html report.",
)
def pytest_configure(config):
htmlpath = config.getoption("htmlpath")
if htmlpath:
for csspath in config.getoption("css"):
if not os.path.exists(csspath):
raise OSError(f"No such file or directory: '{csspath}'")
if not hasattr(config, "workerinput"):
# prevent opening htmlpath on worker nodes (xdist)
config._html = HTMLReport(htmlpath, config)
config.pluginmanager.register(config._html)
def pytest_unconfigure(config):
html = getattr(config, "_html", None)
if html:
del config._html
config.pluginmanager.unregister(html)
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call":
fixture_extras = getattr(item.config, "extras", [])
plugin_extras = getattr(report, "extra", [])
report.extra = fixture_extras + plugin_extras
@pytest.fixture
def extra(pytestconfig):
"""Add details to the HTML reports.
.. code-block:: python
import pytest_html
def test_foo(extra):
extra.append(pytest_html.extras.url("http://www.example.com/"))
"""
pytestconfig.extras = []
yield pytestconfig.extras
del pytestconfig.extras[:]
def data_uri(content, mime_type="text/plain", charset="utf-8"):
data = b64encode(content.encode(charset)).decode("ascii")
return f"data:{mime_type};charset={charset};base64,{data}"
class HTMLReport:
def __init__(self, logfile, config):
logfile = os.path.expanduser(os.path.expandvars(logfile))
self.logfile = os.path.abspath(logfile)
self.test_logs = []
self.title = os.path.basename(self.logfile)
self.results = []
self.errors = self.failed = 0
self.passed = self.skipped = 0
self.xfailed = self.xpassed = 0
has_rerun = config.pluginmanager.hasplugin("rerunfailures")
self.rerun = 0 if has_rerun else None
self.self_contained = config.getoption("self_contained_html")
self.config = config
self.reports = defaultdict(list)
class TestResult:
def __init__(self, outcome, report, logfile, config):
self.test_id = report.nodeid.encode("utf-8").decode("unicode_escape")
if getattr(report, "when", "call") != "call":
self.test_id = "::".join([report.nodeid, report.when])
self.time = getattr(report, "duration", 0.0)
self.formatted_time = self._format_time(report)
self.outcome = outcome
self.additional_html = []
self.links_html = []
self.self_contained = config.getoption("self_contained_html")
self.max_asset_filename_length = int(
config.getini("max_asset_filename_length")
)
self.logfile = logfile
self.config = config
self.row_table = self.row_extra = None
test_index = hasattr(report, "rerun") and report.rerun + 1 or 0
for extra_index, extra in enumerate(getattr(report, "extra", [])):
self.append_extra_html(extra, extra_index, test_index)
self.append_log_html(
report,
self.additional_html,
config.option.capture,
config.option.showcapture,
)
cells = [
html.td(self.outcome, class_="col-result"),
html.td(self.test_id, class_="col-name"),
html.td(self.formatted_time, class_="col-duration"),
html.td(self.links_html, class_="col-links"),
]
self.config.hook.pytest_html_results_table_row(report=report, cells=cells)
self.config.hook.pytest_html_results_table_html(
report=report, data=self.additional_html
)
if len(cells) > 0:
tr_class = None
if self.config.getini("render_collapsed"):
tr_class = "collapsed"
self.row_table = html.tr(cells)
self.row_extra = html.tr(
html.td(self.additional_html, class_="extra", colspan=len(cells)),
class_=tr_class,
)
def __lt__(self, other):
order = (
"Error",
"Failed",
"Rerun",
"XFailed",
"XPassed",
"Skipped",
"Passed",
)
return order.index(self.outcome) < order.index(other.outcome)
def create_asset(
self, content, extra_index, test_index, file_extension, mode="w"
):
asset_file_name = "{}_{}_{}.{}".format(
re.sub(r"[^\w\.]", "_", self.test_id),
str(extra_index),
str(test_index),
file_extension,
)[-self.max_asset_filename_length :]
asset_path = os.path.join(
os.path.dirname(self.logfile), "assets", asset_file_name
)
os.makedirs(os.path.dirname(asset_path), exist_ok=True)
relative_path = f"assets/{asset_file_name}"
kwargs = {"encoding": "utf-8"} if "b" not in mode else {}
with open(asset_path, mode, **kwargs) as f:
f.write(content)
return relative_path
def append_extra_html(self, extra, extra_index, test_index):
href = None
if extra.get("format_type") == extras.FORMAT_IMAGE:
self._append_image(extra, extra_index, test_index)
elif extra.get("format_type") == extras.FORMAT_HTML:
self.additional_html.append(html.div(raw(extra.get("content"))))
elif extra.get("format_type") == extras.FORMAT_JSON:
content = json.dumps(extra.get("content"))
if self.self_contained:
href = data_uri(content, mime_type=extra.get("mime_type"))
else:
href = self.create_asset(
content, extra_index, test_index, extra.get("extension")
)
elif extra.get("format_type") == extras.FORMAT_TEXT:
content = extra.get("content")
if isinstance(content, bytes):
content = content.decode("utf-8")
if self.self_contained:
href = data_uri(content)
else:
href = self.create_asset(
content, extra_index, test_index, extra.get("extension")
)
elif extra.get("format_type") == extras.FORMAT_URL:
href = extra.get("content")
elif extra.get("format_type") == extras.FORMAT_VIDEO:
self._append_video(extra, extra_index, test_index)
if href is not None:
self.links_html.append(
html.a(
extra.get("name"),
class_=extra.get("format_type"),
href=href,
target="_blank",
)
)
self.links_html.append(" ")
def _format_time(self, report):
# parse the report duration into its display version and return
# it to the caller
duration = getattr(report, "duration", None)
if duration is None:
return ""
duration_formatter = getattr(report, "duration_formatter", None)
string_duration = str(duration)
if duration_formatter is None:
if "." in string_duration:
split_duration = string_duration.split(".")
split_duration[1] = split_duration[1][0:2]
string_duration = ".".join(split_duration)
return string_duration
else:
# support %f, since time.strftime doesn't support it out of the box
# keep a precision of 2 for legacy reasons
formatted_milliseconds = "00"
if "." in string_duration:
milliseconds = string_duration.split(".")[1]
formatted_milliseconds = milliseconds[0:2]
duration_formatter = duration_formatter.replace(
"%f", formatted_milliseconds
)
duration_as_gmtime = time.gmtime(report.duration)
return time.strftime(duration_formatter, duration_as_gmtime)
def _populate_html_log_div(self, log, report):
if report.longrepr:
# longreprtext is only filled out on failure by pytest
# otherwise will be None.
# Use full_text if longreprtext is None-ish
# we added full_text elsewhere in this file.
text = report.longreprtext or report.full_text
for line in text.splitlines():
separator = line.startswith("_ " * 10)
if separator:
log.append(line[:80])
else:
exception = line.startswith("E ")
if exception:
log.append(html.span(raw(escape(line)), class_="error"))
else:
log.append(raw(escape(line)))
log.append(html.br())
for section in report.sections:
header, content = map(escape, section)
log.append(f" {header:-^80} ")
log.append(html.br())
if ansi_support():
converter = ansi_support().Ansi2HTMLConverter(
inline=False, escaped=False
)
content = converter.convert(content, full=False)
else:
content = _remove_ansi_escape_sequences(content)
log.append(raw(content))
log.append(html.br())
def append_log_html(
self,
report,
additional_html,
pytest_capture_value,
pytest_show_capture_value,
):
log = html.div(class_="log")
should_skip_captured_output = pytest_capture_value == "no"
if report.outcome == "failed" and not should_skip_captured_output:
should_skip_captured_output = pytest_show_capture_value == "no"
if not should_skip_captured_output:
self._populate_html_log_div(log, report)
if len(log) == 0:
log = html.div(class_="empty log")
log.append("未捕获到日志")
additional_html.append(log)
def _make_media_html_div(
self, extra, extra_index, test_index, base_extra_string, base_extra_class
):
content = extra.get("content")
try:
is_uri_or_path = content.startswith(("file", "http")) or isfile(content)
except ValueError:
# On Windows, os.path.isfile throws this exception when
# passed a b64 encoded image.
is_uri_or_path = False
if is_uri_or_path:
if self.self_contained:
warnings.warn(
"Self-contained HTML report "
"includes link to external "
f"resource: {content}"
)
html_div = html.a(
raw(base_extra_string.format(extra.get("content"))), href=content
)
elif self.self_contained:
src = f"data:{extra.get('mime_type')};base64,{content}"
html_div = raw(base_extra_string.format(src))
else:
content = b64decode(content.encode("utf-8"))
href = src = self.create_asset(
content, extra_index, test_index, extra.get("extension"), "wb"
)
html_div = html.a(
raw(base_extra_string.format(src)),
class_=base_extra_class,
target="_blank",
href=href,
)
return html_div
def _append_image(self, extra, extra_index, test_index):
image_base = '<img src="{}"/>'
html_div = self._make_media_html_div(
extra, extra_index, test_index, image_base, "image"
)
self.additional_html.append(html.div(html_div, class_="image"))
def _append_video(self, extra, extra_index, test_index):
video_base = '<video controls><source src="{}" type="video/mp4"></video>'
html_div = self._make_media_html_div(
extra, extra_index, test_index, video_base, "video"
)
self.additional_html.append(html.div(html_div, class_="video"))
def _appendrow(self, outcome, report):
result = self.TestResult(outcome, report, self.logfile, self.config)
if result.row_table is not None:
index = bisect.bisect_right(self.results, result)
self.results.insert(index, result)
tbody = html.tbody(
result.row_table,
class_="{} results-table-row".format(result.outcome.lower()),
)
if result.row_extra is not None:
tbody.append(result.row_extra)
self.test_logs.insert(index, tbody)
def append_passed(self, report):
if report.when == "call":
if hasattr(report, "wasxfail"):
self.xpassed += 1
self._appendrow("XPassed", report)
else:
self.passed += 1
self._appendrow("Passed", report)
def append_failed(self, report):
if getattr(report, "when", None) == "call":
if hasattr(report, "wasxfail"):
# pytest < 3.0 marked xpasses as failures
self.xpassed += 1
self._appendrow("XPassed", report)
else:
self.failed += 1
self._appendrow("Failed", report)
else:
self.errors += 1
self._appendrow("Error", report)
def append_rerun(self, report):
self.rerun += 1
self._appendrow("Rerun", report)
def append_skipped(self, report):
if hasattr(report, "wasxfail"):
self.xfailed += 1
self._appendrow("XFailed", report)
else:
self.skipped += 1
self._appendrow("Skipped", report)
def _generate_report(self, session):
suite_stop_time = time.time()
suite_time_delta = suite_stop_time - self.suite_start_time
numtests = self.passed + self.failed + self.xpassed + self.xfailed
generated = datetime.datetime.now()
self.style_css = pkg_resources.resource_string(
__name__, os.path.join("resources", "style.css")
).decode("utf-8")
if ansi_support():
ansi_css = [
"\n/******************************",
" * ANSI2HTML STYLES",
" ******************************/\n",
]
ansi_css.extend([str(r) for r in ansi_support().style.get_styles()])
self.style_css += "\n".join(ansi_css)
# <DF> Add user-provided CSS
for path in self.config.getoption("css"):
self.style_css += "\n/******************************"
self.style_css += "\n * CUSTOM CSS"
self.style_css += f"\n * {path}"
self.style_css += "\n ******************************/\n\n"
with open(path) as f:
self.style_css += f.read()
css_href = "assets/style.css"
html_css = html.link(href=css_href, rel="stylesheet", type="text/css")
if self.self_contained:
html_css = html.style(raw(self.style_css))
head = html.head(
html.meta(charset="utf-8"), html.title("测试报告"), html_css
)
class Outcome:
def __init__(
self, outcome, total=0, label=None, test_result=None, class_html=None
):
self.outcome = outcome
self.label = label or outcome
self.class_html = class_html or outcome
self.total = total
self.test_result = test_result or outcome
self.generate_checkbox()
self.generate_summary_item()
def generate_checkbox(self):
checkbox_kwargs = {"data-test-result": self.test_result.lower()}
if self.total == 0:
checkbox_kwargs["disabled"] = "true"
self.checkbox = html.input(
type="checkbox",
checked="true",
onChange="filter_table(this)",
name="filter_checkbox",
class_="filter",
hidden="true",
**checkbox_kwargs,
)
def generate_summary_item(self):
self.summary_item = html.span(
f"{self.total} {self.label}", class_=self.class_html
)
outcomes = [
Outcome("passed", self.passed, label="通过"),
Outcome("skipped", self.skipped, label="跳过"),
Outcome("failed", self.failed, label="失败"),
Outcome("error", self.errors, label="错误"),
Outcome("xfailed", self.xfailed, label="预期失败"),
Outcome("xpassed", self.xpassed, label="预期通过"),
]
if self.rerun is not None:
outcomes.append(Outcome("重跑", self.rerun))
summary = [
html.p(f"执行了{numtests}个测试,耗时{suite_time_delta:.2f}秒"),
html.p(
"(取消)勾选复选框, 以便筛选测试结果",
class_="filter",
hidden="true",
),
]
for i, outcome in enumerate(outcomes, start=1):
summary.append(outcome.checkbox)
summary.append(outcome.summary_item)
if i < len(outcomes):
summary.append(", ")
cells = [
html.th("通过/失败", class_="sortable result initial-sort", col="result"),
html.th("测试用例", class_="sortable", col="name"),
html.th("持续时间", class_="sortable", col="duration"),
html.th("链接", class_="sortable links", col="links"),
]
session.config.hook.pytest_html_results_table_header(cells=cells)
results = [
html.h2("测试结果"),
html.table(
[
html.thead(
html.tr(cells),
html.tr(
[
html.th(
"无测试结果, 试着选择其他测试结果条件",
colspan=len(cells),
)
],
id="not-found-message",
hidden="true",
),
id="results-table-head",
),
self.test_logs,
],
id="results-table",
),
]
main_js = pkg_resources.resource_string(
__name__, os.path.join("resources", "main.js")
).decode("utf-8")
session.config.hook.pytest_html_report_title(report=self)
body = html.body(
html.script(raw(main_js)),
html.h1(self.title),
html.p(
"生成报告时间:{} {}".format(
generated.strftime("%Y-%m-%d"), generated.strftime("%H:%M:%S")
),
),
onl oad="init()",
)
body.extend(self._generate_environment(session.config))
summary_prefix, summary_postfix = [], []
session.config.hook.pytest_html_results_summary(
prefix=summary_prefix, summary=summary, postfix=summary_postfix
)
body.extend([html.h2("用例统计")] + summary_prefix + summary + summary_postfix)
body.extend(results)
doc = html.html(head, body)
unicode_doc = "<!DOCTYPE html>\n{}".format(doc.unicode(indent=2))
# Fix encoding issues, e.g. with surrogates
unicode_doc = unicode_doc.encode("utf-8", errors="xmlcharrefreplace")
return unicode_doc.decode("utf-8")
def _generate_environment(self, config):
if not hasattr(config, "_metadata") or config._metadata is None:
return []
metadata = config._metadata
environment = [html.h2("测试环境")]
rows = []
keys = [k for k in metadata.keys()]
if not isinstance(metadata, OrderedDict):
keys.sort()
for key in keys:
value = metadata[key]
if isinstance(value, str) and value.startswith("http"):
value = html.a(value, href=value, target="_blank")
elif isinstance(value, (list, tuple, set)):
value = ", ".join(str(i) for i in sorted(map(str, value)))
elif isinstance(value, dict):
sorted_dict = {k: value[k] for k in sorted(value)}
value = json.dumps(sorted_dict)
raw_value_string = raw(str(value))
rows.append(html.tr(html.td(key), html.td(raw_value_string)))
environment.append(html.table(rows, id="environment"))
return environment
def _save_report(self, report_content):
dir_name = os.path.dirname(self.logfile)
assets_dir = os.path.join(dir_name, "assets")
os.makedirs(dir_name, exist_ok=True)
if not self.self_contained:
os.makedirs(assets_dir, exist_ok=True)
with open(self.logfile, "w", encoding="utf-8") as f:
f.write(report_content)
if not self.self_contained:
style_path = os.path.join(assets_dir, "style.css")
with open(style_path, "w", encoding="utf-8") as f:
f.write(self.style_css)
def _post_process_reports(self):
for test_name, test_reports in self.reports.items():
outcome = "passed"
wasxfail = False
failure_when = None
full_text = ""
extras = []
duration = 0.0
# in theory the last one should have all logs so we just go
# through them all to figure out the outcome, xfail, duration,
# extras, and when it swapped from pass
for test_report in test_reports:
if test_report.outcome == "rerun":
# reruns are separate test runs for all intensive purposes
self.append_rerun(test_report)
else:
full_text += test_report.longreprtext
extras.extend(getattr(test_report, "extra", []))
duration += getattr(test_report, "duration", 0.0)
if (
test_report.outcome not in ("passed", "rerun")
and outcome == "passed"
):
outcome = test_report.outcome
failure_when = test_report.when
if hasattr(test_report, "wasxfail"):
wasxfail = True
# the following test_report.<X> = settings come at the end of us
# looping through all test_reports that make up a single
# case.
# outcome on the right comes from the outcome of the various
# test_reports that make up this test case
# we are just carrying it over to the final report.
test_report.outcome = outcome
test_report.when = "call"
test_report.nodeid = test_name
test_report.longrepr = full_text
test_report.extra = extras
test_report.duration = duration
if wasxfail:
test_report.wasxfail = True
if test_report.outcome == "passed":
self.append_passed(test_report)
elif test_report.outcome == "skipped":
self.append_skipped(test_report)
elif test_report.outcome == "failed":
test_report.when = failure_when
self.append_failed(test_report)
def pytest_runtest_logreport(self, report):
self.reports[report.nodeid].append(report)
def pytest_collectreport(self, report):
if report.failed:
self.append_failed(report)
def pytest_sessionstart(self, session):
self.suite_start_time = time.time()
def pytest_sessionfinish(self, session):
self._post_process_reports()
report_content = self._generate_report(session)
self._save_report(report_content)
def pytest_terminal_summary(self, terminalreporter):
terminalreporter.write_sep("-", f"generated html file: file://{self.logfile}")
2)覆盖python\Lib\site-packages\pytest_html\resources\main.js
或
C:\Users\tedu\AppData\Roaming\Python\Python38\site-packages\pytest_html...
/* This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this file,
- You can obtain one at http://mozilla.org/MPL/2.0/. */
function toArray(iter) {
if (iter === null) {
return null;
}
return Array.prototype.slice.call(iter);
}
function find(selector, elem) { // eslint-disable-line no-redeclare
if (!elem) {
elem = document;
}
return elem.querySelector(selector);
}
function find_all(selector, elem) {
if (!elem) {
elem = document;
}
return toArray(elem.querySelectorAll(selector));
}
function sort_column(elem) {
toggle_sort_states(elem);
const colIndex = toArray(elem.parentNode.childNodes).indexOf(elem);
let key;
if (elem.classList.contains('result')) {
key = key_result;
} else if (elem.classList.contains('links')) {
key = key_link;
} else {
key = key_alpha;
}
sort_table(elem, key(colIndex));
}
function show_all_extras() { // eslint-disable-line no-unused-vars
find_all('.col-result').forEach(show_extras);
}
function hide_all_extras() { // eslint-disable-line no-unused-vars
find_all('.col-result').forEach(hide_extras);
}
function show_extras(colresult_elem) {
const extras = colresult_elem.parentNode.nextElementSibling;
const expandcollapse = colresult_elem.firstElementChild;
extras.classList.remove('collapsed');
expandcollapse.classList.remove('expander');
expandcollapse.classList.add('collapser');
}
function hide_extras(colresult_elem) {
const extras = colresult_elem.parentNode.nextElementSibling;
const expandcollapse = colresult_elem.firstElementChild;
extras.classList.add('collapsed');
expandcollapse.classList.remove('collapser');
expandcollapse.classList.add('expander');
}
function show_filters() {
const filter_items = document.getElementsByClassName('filter');
for (let i = 0; i < filter_items.length; i++)
filter_items[i].hidden = false;
}
function add_collapse() {
// Add links for show/hide all
const resulttable = find('table#results-table');
const showhideall = document.createElement('p');
showhideall.innerHTML = '显示详情 / ' +
'隐藏详情';
resulttable.parentElement.insertBefore(showhideall, resulttable);
// Add show/hide link to each result
find_all('.col-result').forEach(function(elem) {
const collapsed = get_query_parameter('collapsed') || 'Passed';
const extras = elem.parentNode.nextElementSibling;
const expandcollapse = document.createElement('span');
if (extras.classList.contains('collapsed')) {
expandcollapse.classList.add('expander');
} else if (collapsed.includes(elem.innerHTML)) {
extras.classList.add('collapsed');
expandcollapse.classList.add('expander');
} else {
expandcollapse.classList.add('collapser');
}
elem.appendChild(expandcollapse);
elem.addEventListener('click', function(event) {
if (event.currentTarget.parentNode.nextElementSibling.classList.contains('collapsed')) {
show_extras(event.currentTarget);
} else {
hide_extras(event.currentTarget);
}
});
});
}
function get_query_parameter(name) {
const match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
return match && decodeURIComponent(match[1].replace(/+/g, ' '));
}
function init () { // eslint-disable-line no-unused-vars
reset_sort_headers();
add_collapse();
show_filters();
sort_column(find('.initial-sort'));
find_all('.sortable').forEach(function(elem) {
elem.addEventListener('click',
function() {
sort_column(elem);
}, false);
});
}
function sort_table(clicked, key_func) {
const rows = find_all('.results-table-row');
const reversed = !clicked.classList.contains('asc');
const sorted_rows = sort(rows, key_func, reversed);
/* Whole table is removed here because browsers acts much slower
* when appending existing elements.
*/
const thead = document.getElementById('results-table-head');
document.getElementById('results-table').remove();
const parent = document.createElement('table');
parent.id = 'results-table';
parent.appendChild(thead);
sorted_rows.forEach(function(elem) {
parent.appendChild(elem);
});
document.getElementsByTagName('BODY')[0].appendChild(parent);
}
function sort(items, key_func, reversed) {
const sort_array = items.map(function(item, i) {
return [key_func(item), i];
});
sort_array.sort(function(a, b) {
const key_a = a[0];
const key_b = b[0];
if (key_a == key_b) return 0;
if (reversed) {
return key_a < key_b ? 1 : -1;
} else {
return key_a > key_b ? 1 : -1;
}
});
return sort_array.map(function(item) {
const index = item[1];
return items[index];
});
}
function key_alpha(col_index) {
return function(elem) {
return elem.childNodes[1].childNodes[col_index].firstChild.data.toLowerCase();
};
}
function key_link(col_index) {
return function(elem) {
const dataCell = elem.childNodes[1].childNodes[col_index].firstChild;
return dataCell == null ? '' : dataCell.innerText.toLowerCase();
};
}
function key_result(col_index) {
return function(elem) {
const strings = ['Error', 'Failed', 'Rerun', 'XFailed', 'XPassed',
'Skipped', 'Passed'];
return strings.indexOf(elem.childNodes[1].childNodes[col_index].firstChild.data);
};
}
function reset_sort_headers() {
find_all('.sort-icon').forEach(function(elem) {
elem.parentNode.removeChild(elem);
});
find_all('.sortable').forEach(function(elem) {
const icon = document.createElement('div');
icon.className = 'sort-icon';
icon.textContent = 'vvv';
elem.insertBefore(icon, elem.firstChild);
elem.classList.remove('desc', 'active');
elem.classList.add('asc', 'inactive');
});
}
function toggle_sort_states(elem) {
//if active, toggle between asc and desc
if (elem.classList.contains('active')) {
elem.classList.toggle('asc');
elem.classList.toggle('desc');
}
//if inactive, reset all other functions and add ascending active
if (elem.classList.contains('inactive')) {
reset_sort_headers();
elem.classList.remove('inactive');
elem.classList.add('active');
}
}
function is_all_rows_hidden(value) {
return value.hidden == false;
}
function filter_table(elem) { // eslint-disable-line no-unused-vars
const outcome_att = 'data-test-result';
const outcome = elem.getAttribute(outcome_att);
const class_outcome = outcome + ' results-table-row';
const outcome_rows = document.getElementsByClassName(class_outcome);
for(let i = 0; i < outcome_rows.length; i++){
outcome_rows[i].hidden = !elem.checked;
}
const rows = find_all('.results-table-row').filter(is_all_rows_hidden);
const all_rows_hidden = rows.length == 0 ? true : false;
const not_found_message = document.getElementById('not-found-message');
not_found_message.hidden = !all_rows_hidden;
}
3)覆盖python\Lib\site-packages\pytest_html\resources\style.css
或
C:\Users\tedu\AppData\Roaming\Python\Python38\site-packages\pytest_html...
修改字体颜色
修改\Python38\Lib\site-packages\pytest_html\resources\style.css
body、h1、h2等
修改color
Environment表格
边框:黑色
Results表格
边框:黑色
文字:黑色
设置完成后要重新生成报告
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;
min-width: 1200px;
color: black;
}h1 {
font-size: 24px;
color: black;
}h2 {
font-size: 18px;
color: black;
}p {
color: black;
}a {
color: black;
}table {
border-collapse: collapse;
}/******************************
• SUMMARY INFORMATION
******************************/
environment td {
padding: 5px;
border: 1px solid black;
}
environment tr:nth-child(odd) {
background-color: #f6f6f6;
}
/******************************
• TEST RESULT COLORS
******************************/
span.passed, .passed .col-result {
color: green;
}
span.skipped, span.xfailed, span.rerun, .skipped .col-result, .xfailed .col-result, .rerun .col-result {
color: orange;
}
span.error, span.failed, span.xpassed, .error .col-result, .failed .col-result, .xpassed .col-result {
color: red;
}/******************************
• RESULTS TABLE
•
1. Table Layout
2. Extra
3. Sorting items
•
******************************/
/*------------------
1. Table Layout
------------------/
results-table {
border: 1px solid black;
color: black;
font-size: 16px;
width: 100%
}
results-table th, #results-table td {
padding: 5px;
border: 1px solid black;
text-align: left
}
results-table th {
font-weight: bold
}
/*------------------
1. Extra
------------------/.log:only-child {
height: inherit
}
.log {
background-color: #e6e6e6;
border: 1px solid black;
color: black;
display: block;
font-family: "Courier New", Courier, monospace;
height: 230px;
overflow-y: scroll;
padding: 5px;
white-space: pre-wrap
}
div.image {
border: 1px solid #e6e6e6;
float: right;
height: 240px;
margin-left: 5px;
overflow: hidden;
width: 320px
}
div.image img {
width: 320px
}
.collapsed {
display: none;
}
.expander::after {
content: " (展开详情)";
color: black;
font-style: italic;
cursor: pointer;
}
.collapser::after {
content: " (隐藏详情)";
color: black;
font-style: italic;
cursor: pointer;
}/*------------------
1. Sorting items
------------------/
.sortable {
cursor: pointer;
}.sort-icon {
font-size: 0px;
float: left;
margin-right: 5px;
margin-top: 5px;
/triangle/
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
}.inactive .sort-icon {
/finish triangle/
border-top: 8px solid black;
}.asc.active .sort-icon {
/finish triangle/
border-bottom: 8px solid black;
}.desc.active .sort-icon {
/finish triangle/
border-top: 8px solid black;
}
conftest.文件
修改结果表格字段等,添加数据、改变字符编码
#先执行pytest.ini,再执行conftest.py
import pytest,platform,sys,requests,pymysql,pandas,pytest_html
from py.xml import html
# 测试报告名称
def pytest_html_report_title(report):
report.title = "自定义接口测试报告名称"
# Environment部分配置
def pytest_configure(config):
# 删除项
#config._metadata.pop("JAVA_HOME")
config._metadata.pop("Packages")
config._metadata.pop("Platform")
config._metadata.pop("Plugins")
config._metadata.pop("Python")
# 添加项
config._metadata["平台"] = platform.platform()
config._metadata["Python版本"] = platform.python_version()
config._metadata["包"] = f'Requests({requests.__version__}),PyMySQL({pymysql.__version__}),Pandas({pandas.__version__}),Pytest({pytest.__version__}),Pytest-html({pytest_html.__version__})'
config._metadata["项目名称"] = "自定义项目名称"
# from common.entry import Conf
# config._metadata["测试地址"] = Conf().read_server_conf()
# 在result表格中添加测试描述列
@pytest.mark.optionalhook
def pytest_html_results_table_header(cells): #添加列
cells.insert(1, html.th('测试描述')) #第2列处添加一列,列名测试描述
cells.pop()
# 修改result表格测试描述列的数据来源
@pytest.mark.optionalhook
def pytest_html_results_table_row(report, cells): #添加数据
cells.insert(1, html.td(report.description)) #第2列的数据
cells.pop()
# 修改result表格测试描述列的数据
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
report.description = str(item.function.__doc__) #函数注释文档字符串
# # 测试统计部分添加测试部门和人员
# @pytest.mark.optionalhook
# def pytest_html_results_summary(prefix):
# prefix.extend([html.p("所属部门: 自动化测试部")])
# prefix.extend([html.p("测试人员: ***")])
# 解决参数化时汉字不显示问题
def pytest_collection_modifyitems(items):
#下面3行只能解决控制台中,参数化时汉字不显示问题
# for item in items:
# item.name = item.name.encode('utf-8').decode('unicode-escape')
# item._nodeid = item.nodeid.encode('utf-8').decode('unicode-escape')
#下面3行只能解决测试报告中,参数化时汉字不显示问题
outcome = yield
report = outcome.get_result()
report.nodeid = report.nodeid.encode("utf-8").decode("unicode_escape")
执行测试生成测试报告
from calc import Calc
import pytest
# 测试固件:每次执行测试之前都要创建一个Calc对象
@pytest.fixture(autouse=True) # 测试固件/测试夹具
def create_calc_obj():
global c # 参考上一段代码中的全局变量的基础只是
c = Calc()
cases_add = [[1, 2, 3], [1, 0, 1], [-1, 2, 1], [-1, -2, -3]] # 加法的用例
cases_sub = [[1, 0, 1], [1, 2, -1], [3, 2, 1], [2, 3, -1]] # 减法的用例
@pytest.mark.parametrize('a,b,expect', cases_add) # 参数化装饰器 封装了循环.一次取一条用例(小列表),把一条用例拆到/解包到引号中的变量中
def test_add(a, b, expect): # 这里有四条用例,本测试函数执行4次
actual = c.add(a, b)
assert actual == expect, f'加法用例(a,b,expect)==执行失败==预期:{expect}==实际:{actual}'
print(f'加法用例(a,b,expect)==执行通过')
@pytest.mark.parametrize('a,b,expect', cases_sub) # 参数化装饰器
def test_sub(a, b, expect): # 减法用例
actual = c.sub(a, b)
assert actual == expect, f'减法用例(a,b,expect)==执行失败==预期:{expect}==实际:{actual}'
print(f'减法用例(a,b,expect)==执行通过')
if __name__ == '__main__':
pytest.main(['-sv', '--tb=line', '--html=calc.html','demo04.py'])
作者:暄总-tester