HTMLTestRunner python单元测试报告代码
下面我写的是我自己改了以后的,这个链接是我没修改之前的!
源代码下载地址:http://tungwaiyip.info/software/HTMLTestRunner.html
补更,求关注!!!
效果图
废话不多说直接看效果图 ↓
使用方法
在当前路径下创建几个Case开头的.py文件
调用代码
# -*- coding: utf-8 -*-
import unittest
from utils import HTML_REPORT
output_result_path = "C:\\Users\\Administrator\\Desktop\\ZWCMS_SELENIUM_TEST_RESULT.html"
output_result_title = "test_title"
if __name__ == '__main__':
suite = unittest.TestSuite() # 创建测试套件
all_cases = unittest.defaultTestLoader.discover('.', 'Case*.py')
# 找到目录下所有的以 test 开头的 Python 文件里面的测试用例
for case in all_cases:
suite.addTests(case) # 把所有的测试用例添加进来
print(output_result_path)
fp = open(output_result_path, 'wb')
runner = HTML_REPORT.HTMLTestRunner(stream=fp, title=output_result_title)
runner.run(suite)
测试代码
测试一下,可复制成多个文件,文件名与 class 名不能冲突。
# -*- coding: utf-8 -*-
import unittest, time
class Test_Login(unittest.TestCase):
def setUp(self):
pass
def test_1_base(self):
assert 1 == 1
def test_2_base(self):
assert 1 == 1
def test_3_base(self):
assert 1 != 1
def tearDown(self):
pass
if __name__ == "__main__":
unittest.main()
修改后的代码
# coding=utf-8
__author__ = u"那一丝寒意,冰封千里"
__version__ = "0.9.0"
# TODO: color stderr
# TODO: simplify javascript using ,ore than 1 class in the class attribute?
# coding=utf-8
import datetime
import io
import sys
import time
import unittest
import re
from xml.sax import saxutils
# ------------------------------------------------------------------------
# The redirectors below are used to capture output during testing. Output
# sent to sys.stdout and sys.stderr are automatically captured. However
# in some cases sys.stdout is already cached before HTMLTestRunner is
# invoked (e.g. calling logging.basicConfig). In order to capture those
# output, use the redirectors for the cached stream.
#
# e.g.
# >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
# >>>
class OutputRedirector(object):
""" Wrapper to redirect stdout or stderr """
def __init__(self, fp):
self.fp = fp
def write(self, s):
self.fp.write(s.encode())
def writelines(self, lines):
self.fp.writelines(lines)
def flush(self):
self.fp.flush()
stdout_redirector = OutputRedirector(sys.stdout)
stderr_redirector = OutputRedirector(sys.stderr)
# ----------------------------------------------------------------------
# Template
class Template_mixin(object):
"""
Define a HTML template for report customerization and generation.
Overall structure of an HTML report
HTML
+------------------------+
|<html> |
| <head> |
| |
| STYLESHEET |
| +----------------+ |
| | | |
| +----------------+ |
| |
| </head> |
| |
| <body> |
| |
| HEADING |
| +----------------+ |
| | | |
| +----------------+ |
| |
| REPORT |
| +----------------+ |
| | | |
| +----------------+ |
| |
| ENDING |
| +----------------+ |
| | | |
| +----------------+ |
| |
| </body> |
|</html> |
+------------------------+
"""
STATUS = {
0: '通过',
1: '失败',
2: '错误',
3: '跳过'
}
DEFAULT_TITLE = 'Unit Test Report'
DEFAULT_DESCRIPTION = ''
# ------------------------------------------------------------------------
# HTML Template
HTML_TMPL = r"""
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>%(title)s</title>
<meta name="generator" content="%(generator)s"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link href="http://libs.baidu.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="http://libs.baidu.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
%(stylesheet)s
</head>
<body >
<script language="javascript" type="text/javascript">
output_list = Array();
// 默认显示所有的
let Case_level = 'a';
/*level 调整增加只显示通过用例的分类 --Findyou
0 概要 全部展开显示标题
2 通过
1 失败
4 错误
5 跳过
3 所有 全部展开显示所有信息
*/
function showCase(level) {
trs = document.getElementsByTagName("tr");
for (var i = 0; i < trs.length; i++) {
tr = trs[i];
id = tr.id;
id_sub = id.substr(0, 2);
if (id_sub == 'pt' || id_sub == 'ft' || id_sub == 'st' || id_sub == 'et') {
// 展开所有通过的用例
if (id_sub == 'pt') {
if (level == 2) {
Case_level = 'p';
tr.className = '';
} else {
tr.className = 'hiddenRow';
}
}
// 展开所有失败的用例
if (id_sub == 'ft') {
if (level == 1) {
Case_level = 'f';
tr.className = '';
} else {
tr.className = 'hiddenRow';
}
}
// 展开所有跳过的用例
if (id_sub == 'st') {
if (level == 5) {
Case_level = 's';
tr.className = '';
} else {
tr.className = 'hiddenRow';
}
}
// 展开所有错误的用例
if (id_sub == 'et') {
if (level == 4) {
Case_level = 'e';
tr.className = '';
} else {
tr.className = 'hiddenRow';
}
}
// 展开所有用例
if (level == 3) {
Case_level = 'a';
tr.className = '';
}
}
}
//加入【详细】切换文字变化 --Findyou
detail_class=document.getElementsByClassName('detail');
//console.log(detail_class.length)
if (level == 3) {
for (var i = 0; i < detail_class.length; i++){
detail_class[i].innerHTML="收起"
}
}
else{
for (var i = 0; i < detail_class.length; i++){
detail_class[i].innerHTML="详细"
}
}
}
function showClassDetail(cid, count) {
let tr;
let tid;
var id_list = Array();
var toHide = 1;
for (var i = 0; i < count; i++) {
//ID修改 点 为 下划线 -Findyou
const tid0 = 't' + cid.substr(1) + '_' + (i + 1);
if (Case_level == 'a') {
tid = 'f' + tid0;
tr = document.getElementById(tid);
if (!tr) {
tid = 'p' + tid0;
tr = document.getElementById(tid);
}
if (!tr) {
tid = 's' + tid0;
tr = document.getElementById(tid);
}
if (!tr) {
tid = 'e' + tid0;
tr = document.getElementById(tid);
}
} else {
tid = Case_level + tid0;
tr = document.getElementById(tid)
}
if(tr){
id_list.push(tid) ;
// 判断到的标签是隐藏还是显示
if (tr.className) {
toHide = 0;
}
}
}
for (var i = 0; i < id_list.length; i++) {
tid = id_list[i];
//修改点击无法收起的BUG,加入【详细】切换文字变化 --Findyou
if (toHide) {
document.getElementById(tid).className = 'hiddenRow';
document.getElementById(cid).innerText = "详细"
}
else {
document.getElementById(tid).className = '';
document.getElementById(cid).innerText = "收起"
}
}
}
function html_escape(s) {
s = s.replace(/&/g,'&');
s = s.replace(/</g,'<');
s = s.replace(/>/g,'>');
return s;
}
</script>
%(heading)s
%(report)s
%(ending)s
</body>
</html>
"""
# variables: (title, generator, stylesheet, heading, report, ending)
# ------------------------------------------------------------------------
# Stylesheet
#
# alternatively use a <link> for external style sheet, e.g.
# <link rel="stylesheet" href="$url" type="text/css">
STYLESHEET_TMPL = """
<style type="text/css" media="screen">
body { font-family: Microsoft YaHei,Tahoma,arial,helvetica,sans-serif;padding: 20px; font-size: 80%; }
table { font-size: 100%; }
/* -- heading ---------------------------------------------------------------------- */
.heading {
margin-top: 0ex;
margin-bottom: 1ex;
}
.heading .description {
margin-top: 4ex;
margin-bottom: 6ex;
}
/* -- report ------------------------------------------------------------------------ */
#total_row { font-weight: bold; }
.passCase { color: #5cb85c; font-weight: bold; }
.errorCase { color: #ff2222; font-weight: bold; }
.failCase { color: #f0ad4e; font-weight: bold; }
.skipCase { color: #aaaaaa; font-weight: bold; }
.hiddenRow { display: none; }
.testcase { margin-left: 2em; }
.passCase_button { background: #5cb85c; font-weight: bold; color:#fff;}
.errorCase_button { background: #ff2222; font-weight: bold; color:#fff;}
.failCase_button { background: #f0ad4e; font-weight: bold; color:#fff;}
.skipCase_button { background: #aaaaaa; font-weight: bold; color:#fff;}
</style>
"""
# # ------------------------------------------------------------------------
# # Heading
# #
#
# HEADING_TMPL = """
# <div class='heading'>
# <h1 style="font-family: Microsoft YaHei">%(title)s</h1>
# %(parameters)s
# <p class='description'>%(description)s</p>
# </div>
#
# """
# ------------------------------------------------------------------------
# Heading
#
HEADING_TMPL = """<div class='heading'>
<h1>%(title)s</h1>
%(parameters)s
<p class='description'>%(description)s</p>
</div>
""" # variables: (title, parameters, description)
HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s : </strong> %(value)s</p>
""" # variables: (name, value)
# ------------------------------------------------------------------------
# Report
#
REPORT_TMPL = """
<p id='show_detail_line'>
<a class="btn btn-primary" href='javascript:showCase(0)'>概要{ %(passrate)s }</a>
<a class="btn btn-success" style='background:#5cb85c' href='javascript:showCase(2)'>通过{ %(Pass)s }</a>
<a class="btn btn-danger" style='background:#f0ad4e' href='javascript:showCase(1)'>失败{ %(fail)s }</a>
<a class="btn btn-danger" style='background:#ff2222' href='javascript:showCase(4)'>错误{ %(error)s }</a>
<a class="btn btn-warning" style='background:#aaaaaa' href='javascript:showCase(5)'>跳过{ %(skip)s }</a>
<a class="btn btn-info" href='javascript:showCase(3)'>所有{ %(count)s }</a>
</p>
<table id='result_table' class="table table-condensed table-bordered table-hover">
<colgroup>
<col align='left' />
<col align='right' />
<col align='right' />
<col align='right' />
<col align='right' />
<col align='right' />
</colgroup>
<tr id='header_row' class="text-center success" style="font-weight: bold;font-size: 14px;">
<td>用例集/测试用例</td>
<td>总计</td>
<td>通过</td>
<td>失败</td>
<td>错误</td>
<td>跳过</td>
<td>详细</td>
</tr>
%(test_list)s
<tr id='total_row' class="text-center active">
<td>总计</td>
<td>%(count)s</td>
<td>%(Pass)s</td>
<td>%(fail)s</td>
<td>%(error)s</td>
<td>%(skip)s</td>
<td>通过率:%(passrate)s</td>
</tr>
</table>
""" # variables: (test_list, count, Pass, fail, error)
REPORT_CLASS_TMPL = r"""
<tr class='%(style)s warning'>
<td>%(desc)s</td>
<td class="text-center">%(count)s</td>
<td class="text-center">%(Pass)s</td>
<td class="text-center">%(fail)s</td>
<td class="text-center">%(error)s</td>
<td class="text-center">%(skip)s</td>
<td class="text-center"><a href="javascript:showClassDetail('%(cid)s',%(count)s)" class="detail" id='%(cid)s'>详细</a></td>
</tr>
""" # variables: (style, desc, count, Pass, fail,skip, error, cid)
REPORT_TEST_WITH_OUTPUT_TMPL = r"""
<tr id='%(tid)s' class='%(Class)s'>
<td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
<td colspan='5' align='center'>
<!--默认收起错误信息 -Findyou -->
<button id='btn_%(tid)s' type="button" class="btn btn-xs %(style)s_button" data-toggle="collapse" data-target='#div_%(tid)s'>%(status)s</button>
<div id='div_%(tid)s' class="collapse">
<pre>
%(script)s
</pre>
</div>
</td>
</tr>
""" # variables: (tid, Class, style, desc, status)
REPORT_TEST_NO_OUTPUT_TMPL = r"""
<tr id='%(tid)s' class='%(Class)s'>
<td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
<td colspan='5' align='center'><span class="label label-success success">%(status)s</span></td>
<td align='center'><a %(hidde)s href="%(image)s">picture_shot</a></td>
</tr>
""" # variables: (tid, Class, style, desc, status)
REPORT_TEST_OUTPUT_TMPL = r"""
%(output)s
"""
# variables: (id, output)
# ------------------------------------------------------------------------
# ENDING
#
ENDING_TMPL = """
<div id='ending'> </div>
<div style=" position:fixed;right:50px; bottom:30px; width:20px; height:20px;cursor:pointer">
<a href="#">
<span class="glyphicon glyphicon-eject" style = "font-size:30px;" aria-hidden="true"></span>
</a>
</div>
"""
# -------------------- The end of the Template class -------------------
TestResult = unittest.TestResult
class _TestResult(TestResult):
# note: _TestResult is a pure representation of results.
# It lacks the output and reporting ability compares to unittest._TextTestResult.
def __init__(self, verbosity=1):
TestResult.__init__(self)
super().__init__(verbosity)
self.stdout0 = None
self.stderr0 = None
self.success_count = 0
self.skipped_count = 0 # add skipped_count
self.failure_count = 0
self.error_count = 0
self.verbosity = verbosity
# result is a list of result in 4 tuple
# (
# result code (0: success; 1: fail; 2: error),
# TestCase object,
# Test output (byte string),
# stack trace,
# )
self.result = []
def startTest(self, test):
TestResult.startTest(self, test)
# just one buffer for both stdout and stderr
self.outputBuffer = io.BytesIO()
stdout_redirector.fp = self.outputBuffer
stderr_redirector.fp = self.outputBuffer
self.stdout0 = sys.stdout
self.stderr0 = sys.stderr
sys.stdout = stdout_redirector
sys.stderr = stderr_redirector
def complete_output(self):
"""
Disconnect output redirection and return buffer.
Safe to call multiple times.
"""
if self.stdout0:
sys.stdout = self.stdout0
sys.stderr = self.stderr0
self.stdout0 = None
self.stderr0 = None
return self.outputBuffer.getvalue()
def stopTest(self, test):
# Usually one of addSuccess, addError or addFailure would have been called.
# But there are some path in unittest that would bypass this.
# We must disconnect stdout in stopTest(), which is guaranteed to be called.
self.complete_output()
def addSuccess(self, test):
self.success_count += 1
TestResult.addSuccess(self, test)
output = self.complete_output()
self.result.append((0, test, output, ''))
if self.verbosity > 1:
sys.stderr.write('P ')
sys.stderr.write(str(test))
sys.stderr.write('\n')
# else:
# sys.stderr.write('P')
def addSkip(self, test, reason):
self.skipped_count += 1
TestResult.addSkip(self, test, reason)
output = self.complete_output()
self.result.append((3, test, '', reason))
if self.verbosity > 1:
sys.stderr.write('skip ')
sys.stderr.write(str(test))
sys.stderr.write('\n')
# else:
# sys.stderr.write('S')
def addError(self, test, err):
self.error_count += 1
TestResult.addError(self, test, err)
_, _exc_str = self.errors[-1]
output = self.complete_output()
self.result.append((2, test, output, _exc_str))
if self.verbosity > 1:
sys.stderr.write('E ')
sys.stderr.write(str(test))
sys.stderr.write('\n')
# else:
# sys.stderr.write('E')
def addFailure(self, test, err):
self.failure_count += 1
TestResult.addFailure(self, test, err)
_, _exc_str = self.failures[-1]
output = self.complete_output()
self.result.append((1, test, output, _exc_str))
if self.verbosity > 1:
sys.stderr.write('F ')
sys.stderr.write(str(test))
sys.stderr.write('\n')
class HTMLTestRunner(Template_mixin):
"""
"""
def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None, name=None):
self.stream = stream
self.verbosity = verbosity
if title is None:
self.title = self.DEFAULT_TITLE
else:
self.title = title
if name is None:
self.name = ''
else:
self.name = name
if description is None:
self.description = self.DEFAULT_DESCRIPTION
else:
self.description = description
self.startTime = datetime.datetime.now()
def run(self, test):
"Run the given test case or test suite."
result = _TestResult(self.verbosity)
test(result)
self.stopTime = datetime.datetime.now()
self.generateReport(test, result)
# print (sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime))
return result
def sortResult(self, result_list):
# unittest does not seems to run in any particular order.
# Here at least we want to group them together by class.
rmap = {}
classes = []
for n, t, o, e in result_list:
cls = t.__class__
if not cls in rmap:
rmap[cls] = []
classes.append(cls)
rmap[cls].append((n, t, o, e))
r = [(cls, rmap[cls]) for cls in classes]
return r
def getReportAttributes(self, result):
"""
Return report attributes as a list of (name, value).
Override this to add custom attributes.
"""
startTime = str(self.startTime)[:19]
duration = str(self.stopTime - self.startTime)
status = []
if result.success_count: status.append('Pass %s' % result.success_count)
if result.failure_count: status.append('Failure %s' % result.failure_count)
if result.skipped_count: status.append('Skip %s' % result.skipped_count)
if result.error_count: status.append('Error %s' % result.error_count)
if status:
status = ' '.join(status)
else:
status = 'none'
return [
('Start Time', startTime),
('Duration', duration),
('Status', status),
]
def generateReport(self, test, result):
report_attrs = self.getReportAttributes(result) # 报告的头部
generator = 'HTMLTestRunner %s' % __version__
stylesheet = self._generate_stylesheet() # 拿到css文件
heading = self._generate_heading(report_attrs)
report = self._generate_report(result)
ending = self._generate_ending()
output = self.HTML_TMPL % dict(
title=saxutils.escape(self.title),
generator=generator,
stylesheet=stylesheet,
heading=heading,
report=report,
ending=ending,
)
self.stream.write(output.encode('utf8'))
def _generate_stylesheet(self):
return self.STYLESHEET_TMPL
def _generate_heading(self, report_attrs):
a_lines = []
for name, value in report_attrs:
line = self.HEADING_ATTRIBUTE_TMPL % dict(
name=saxutils.escape(name),
value=saxutils.escape(value),
)
a_lines.append(line)
heading = self.HEADING_TMPL % dict(
title=saxutils.escape(self.title),
parameters=''.join(a_lines),
description=saxutils.escape(self.description),
)
return heading
# 根据result收集报告
def _generate_report(self, result):
rows = []
sortedResult = self.sortResult(result.result)
i = 0
for cid, (cls, cls_results) in enumerate(sortedResult):
# subtotal for a class
np = nf = ns = ne = 0 # np代表pass个数,nf代表fail,ns代表skip,ne,代表error
for n, t, o, e in cls_results:
if n == 0:
np += 1
elif n == 1:
nf += 1
elif n == 3:
ns += 1
else:
ne += 1
# format class description
# if cls.__module__ == "__main__":
# name = cls.__name__
# else:
# name = "%s.%s" % (cls.__module__, cls.__name__)
name = cls.__name__
try:
core_name = self.name[i]
except Exception:
core_name = ''
# doc = (cls.__doc__)+core_name and (cls.__doc__+core_name).split("\n")[0] or ""
doc = (cls.__doc__) and cls.__doc__.split("\n")[0] or ""
desc = doc and '%s: %s' % (name, doc) or name
i = i + 1 # 生成每个TestCase类的汇总数据,对于报告中的
row = self.REPORT_CLASS_TMPL % dict(
style=ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
desc=desc,
count=np + nf + ne + ns,
Pass=np,
fail=nf,
error=ne,
skip=ns,
cid='c%s' % (cid + 1),
)
rows.append(row)
# 生成每个TestCase类中所有方法的测试结果
for tid, (n, t, o, e) in enumerate(cls_results):
self._generate_report_test(rows, cid, tid, n, t, o, e)
count = str(result.success_count + result.failure_count + result.error_count + result.skipped_count)
passrate = round(int(result.success_count) / int(count) * 100, 1)
report = self.REPORT_TMPL % dict(
test_list=''.join(rows),
passrate=str(passrate) + "%",
count=count,
Pass=str(result.success_count),
fail=str(result.failure_count),
error=str(result.error_count),
skip=str(result.skipped_count)
)
return report
def _generate_report_test(self, rows, cid, tid, n, t, o, e):
# e.g. 'pt1.1', 'ft1.1', etc
has_output = bool(o or e)
tid = (n == 0 and 'p' or n == 1 and 'f' or n == 2 and 'e' or 's') + 't%s_%s' % (cid + 1, tid + 1)
name = t.id().split('.')[-1]
doc = t.shortDescription() or ""
desc = doc and ('%s: %s' % (name, doc)) or name
# tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
tmpl = self.REPORT_TEST_WITH_OUTPUT_TMPL
uo1 = ""
# o and e should be byte string because they are collected from stdout and stderr?
if isinstance(o, bytes):
o = str(o, 'utf-8')
if isinstance(o, str):
uo = o
else:
uo = e
if isinstance(e, str):
# TODO: some problem with 'string_escape': it escape \n and mess up formating
# ue = unicode(e.encode('string_escape'))
ue = e
else:
ue = o
output = saxutils.escape(str(uo) + str(ue))
if output == '':
output = "this Case not OutPut Code!"
script = self.REPORT_TEST_OUTPUT_TMPL % dict(
output=output
)
if "shot_picture_name" in str(saxutils.escape(str(ue))):
hidde_status = ''
pattern = re.compile(r'AssertionError:.*?shot_picture_name=(.*)', re.S)
shot_name = re.search(pattern, str(saxutils.escape(str(e))))
try:
image_url = "http://192.168.99.105/contractreport/screenshot/" + time.strftime("%Y-%m-%d",
time.localtime(
time.time())) + "/" + shot_name.group(
1) + ".png"
except Exception:
image_url = "http://192.168.99.105/contractreport/screenshot/" + time.strftime("%Y-%m-%d",
time.localtime(
time.time()))
# style= n == 2 and 'errorCase' or (n == 1 and 'failCase') or (n == 3 and 'skipCase' or 'none')
else:
hidde_status = '''hidden="hidden"'''
image_url = ''
# Class=(n == 0 and 'hiddenRow' or 'none'),
row = tmpl % dict(
tid=tid,
Class='hiddenRow',
style=(n == 0 and 'passCase' or n == 1 and 'failCase' or n == 2 and 'errorCase' or 'skipCase'),
desc=desc,
script=script,
hidde=hidde_status,
image=image_url,
status=self.STATUS[n],
)
rows.append(row)
if not has_output:
return
def _generate_ending(self):
return self.ENDING_TMPL