首页 > 其他分享 >Pytest23--生成测试报告

Pytest23--生成测试报告

时间:2022-12-22 11:03:41浏览次数:82  
标签:测试报告 extra -- self pytest test html report Pytest23

生成测试报告

测试报告中至少可以显示执行了多少条用例,用例信息如何,多少条用例执行成功,多少条用例执行失败,多少用例出错

使用步骤

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

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



标签:测试报告,extra,--,self,pytest,test,html,report,Pytest23
From: https://blog.51cto.com/u_14911999/5962411

相关文章

  • 机床行业MES系统对企业的帮助
    高精度数字化机床是现代生产不可缺少的组成部分。机床行业现在常遇到的的有问题有:1. 停机、换装调试、暂停机、减速等售后服务响应慢,客户满意度低2. 设备操作、维保不当造......
  • 开机密码忘了咋办,直接拔电源,结果呢???????
    Windows密码忘了怎么办?​​一、5次shift键弹出粘滞键​​二、异常断电触发系统的自动修复三、未登录修改系统文件四、登录界面5次shift键弹出cmd五、思路总结我有一个朋友,绝......
  • 基于ROS的Mako相机,Basler相机,禾赛激光雷达,Velodyne激光雷达,速腾激光雷达,Delphi毫米波
    把大象放冰箱分为几步?三部,那么我们对传感器进行安装调试分为几步?答案也是三步。第一步,安装驱动第二步,运行ROS代码第三步,调试效果用到的传感器型号与官网如下:传感器型号官......
  • 嵌入式:ARM多寄存器存取指令详解
    多寄存器传送指令可以用一条指令将16个可见寄存器(R0~R15)的任意子集合(或全部)存储到存储器或从存储器中读取数据到该寄存器集合中。如:可将寄存器列表保存到堆栈,也可将寄存器列......
  • 如何在云原生环境中实现安全左移?
    在过去几年里,勒索软件一直是企业安全团队关心的头等大事,而当前软件漏洞问题数量也在逐渐抬头。基于云的应用程序和服务的爆发式增长以及数字化工作的增加,对黑客来说是一大......
  • Android 之 进度条样式
    例1:(默认样式(中等圆形))Xml代码Xml代码<ProgressBarandroid:id="@+id/progressBar1"......
  • 你好Avalonia框架
    https://docs.avaloniaui.net/docs/getting-started/起因公司事业部是做移动等营业厅办理相关业务,无纸化系统的.简单的说就是以前去营业厅办理业务都需要各种打印文件然......
  • k8s yaml资源清单格式
    k8s由于资源比较多,组合起来参数众多,不适合用cli传参的形式。因此用yaml文件的形式传参给k8s。yaml文件相当于剧本,运维人员相当于制片人,k8s相当于导演,docker相当于剧务、po......
  • Gartner对低代码平台的11项关键能力要求
    Gartner在此前的《企业级低代码开发平台的关键能力报告》(CriticalCapabilitiesforEnterpriseLow-CodeApplicationPlatforms)中,定义了低代码的11项关键能力。1. Int......
  • Vue某列内容以html的形式展示(举例:超链接、a标签)
    1、显示html的方法:将该列的type设成html,那么当给此列赋值为带html标签的内容的时候,html标签不会以文本的形式展示在页面上template:<vxe-table-columnfield="attachmen......