大数跨境
0
0

HTMLTestRunner修改成Python3版本

HTMLTestRunner修改成Python3版本 橙子好甜
2025-07-07
2
导读:HTMLTestRunner 是 Python unittest 的扩展,用于生成美观的 HTML 测试报告。

HTMLTestRunner 是 Python unittest 的扩展,用于生成美观的 HTML 测试报告。它将控制台输出的文本结果转换为可视化网页,用颜色区分通过/失败用例,展示执行时间、统计信息等,便于阅读和分享。兼容标准 unittest,只需替换 TextTestRunner 即可使用,适合需要简单直观展示测试结果的场景,提升团队协作效率。

python3的HTMLTestRunner.py


import datetimeimport ioimport sysimport timeimport unittestfrom xml.sax import saxutils



class OutputRedirector(object):    """ Wrapper to redirect stdout or stderr """    def __init__(self, fp):        self.fp = fp
    def write(self, s):        self.fp.write(s)
    def writelines(self, lines):        self.fp.writelines(lines)
    def flush(self):        self.fp.flush()
stdout_redirector = OutputRedirector(sys.stdout)stderr_redirector = OutputRedirector(sys.stderr)


__version__="0.8.2"# 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'pass',    1'fail',    2'error',    }
    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"/>    %(stylesheet)s</head><body><script language="javascript" type="text/javascript"><!--output_list = Array();
/* level - 0:Summary; 1:Failed; 2:All */function showCase(level) {    trs = document.getElementsByTagName("tr");    for (var i = 0; i < trs.length; i++) {        tr = trs[i];        id = tr.id;        if (id.substr(0,2) == 'ft') {            if (level < 1) {                tr.className = 'hiddenRow';            }            else {                tr.className = '';            }        }        if (id.substr(0,2) == 'pt') {            if (level > 1) {                tr.className = '';            }            else {                tr.className = 'hiddenRow';            }        }    }}

function showClassDetail(cid, count) {    var id_list = Array(count);    var toHide = 1;    for (var i = 0; i < count; i++) {        tid0 = 't' + cid.substr(1) + '.' + (i+1);        tid = 'f' + tid0;        tr = document.getElementById(tid);        if (!tr) {            tid = 'p' + tid0;            tr = document.getElementById(tid);        }        id_list[i] = tid;        if (tr.className) {            toHide = 0;        }    }    for (var i = 0; i < count; i++) {        tid = id_list[i];        if (toHide) {            document.getElementById('div_'+tid).style.display = 'none'            document.getElementById(tid).className = 'hiddenRow';        }        else {            document.getElementById(tid).className = '';        }    }}

function showTestDetail(div_id){    var details_div = document.getElementById(div_id)    var displayState = details_div.style.display    // alert(displayState)    if (displayState != 'block' ) {        displayState = 'block'        details_div.style.display = 'block'    }    else {        details_div.style.display = 'none'    }}

function html_escape(s) {    s = s.replace(/&/g,'&amp;');    s = s.replace(/</g,'&lt;');    s = s.replace(/>/g,'&gt;');    return s;}
/* obsoleted by detail in <div>function showOutput(id, name) {    var w = window.open("", //url                    name,                    "resizable,scrollbars,status,width=800,height=450");    d = w.document;    d.write("<pre>");    d.write(html_escape(output_list[id]));    d.write("\n");    d.write("<a href='javascript:window.close()'>close</a>\n");    d.write("</pre>\n");    d.close();}*/--></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: verdana, arial, helvetica, sans-serif; font-size: 80%; }table       { font-size: 100%; }pre         { }
/* -- heading ---------------------------------------------------------------------- */h1 {    font-size: 16pt;    color: gray;}.heading {    margin-top: 0ex;    margin-bottom: 1ex;}
.heading .attribute {    margin-top: 1ex;    margin-bottom: 0;}
.heading .description {    margin-top: 4ex;    margin-bottom: 6ex;}
/* -- css div popup ------------------------------------------------------------------------ */a.popup_link {}
a.popup_link:hover {    color: red;}
.popup_window {    display: none;    position: relative;    left: 0px;    top: 0px;    /*border: solid #627173 1px; */    padding: 10px;    background-color: #E6E6D6;    font-family: "Lucida Console", "Courier New", Courier, monospace;    text-align: left;    font-size: 8pt;    width: 500px;}
}/* -- report ------------------------------------------------------------------------ */#show_detail_line {    margin-top: 3ex;    margin-bottom: 1ex;}#result_table {    width: 80%;    border-collapse: collapse;    border: 1px solid #777;}#header_row {    font-weight: bold;    color: white;    background-color: #777;}#result_table td {    border: 1px solid #777;    padding: 2px;}#total_row  { font-weight: bold; }.passClass  { background-color: #6c6; }.failClass  { background-color: #c60; }.errorClass { background-color: #c00; }.passCase   { color: #6c6; }.failCase   { color: #c60; font-weight: bold; }.errorCase  { color: #c00; font-weight: bold; }.hiddenRow  { display: none; }.testcase   { margin-left: 2em; }

/* -- ending ---------------------------------------------------------------------- */#ending {}
</style>"""


    # ------------------------------------------------------------------------    # 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'>Show<a href='javascript:showCase(0)'>Summary</a><a href='javascript:showCase(1)'>Failed</a><a href='javascript:showCase(2)'>All</a></p><table id='result_table'><colgroup><col align='left' /><col align='right' /><col align='right' /><col align='right' /><col align='right' /><col align='right' /></colgroup><tr id='header_row'>    <td>Test Group/Test case</td>    <td>Count</td>    <td>Pass</td>    <td>Fail</td>    <td>Error</td>    <td>View</td></tr>%(test_list)s<tr id='total_row'>    <td>Total</td>    <td>%(count)s</td>    <td>%(Pass)s</td>    <td>%(fail)s</td>    <td>%(error)s</td>    <td>&nbsp;</td></tr></table>""" # variables: (test_list, count, Pass, fail, error)
    REPORT_CLASS_TMPL = r"""<tr class='%(style)s'>    <td>%(desc)s</td>    <td>%(count)s</td>    <td>%(Pass)s</td>    <td>%(fail)s</td>    <td>%(error)s</td>    <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td></tr>""" # variables: (style, desc, count, Pass, fail, 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'>
    <!--css div popup start-->    <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >        %(status)s</a>
    <div id='div_%(tid)s' class="popup_window">        <div style='text-align: right; color:red;cursor:pointer'>        <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >           [x]</a>        </div>        <pre>        %(script)s        </pre>    </div>    <!--css div popup end-->
    </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'>%(status)s</td></tr>""" # variables: (tid, Class, style, desc, status)

    REPORT_TEST_OUTPUT_TMPL = r"""%(id)s: %(output)s""" # variables: (id, output)


    # ------------------------------------------------------------------------    # ENDING    #
    ENDING_TMPL = """<div id='ending'>&nbsp;</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)        self.stdout0 = None        self.stderr0 = None        self.success_count = 0        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.StringIO()        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('ok ')            sys.stderr.write(str(test))            sys.stderr.write('\n')        else:            sys.stderr.write('.')
    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')        else:            sys.stderr.write('F')

class HTMLTestRunner(Template_mixin):    """    """    def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):        self.stream = stream        self.verbosity = verbosity        if title is None:            self.title = self.DEFAULT_TITLE        else:            self.title = title        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))# 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.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()        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

    def _generate_report(self, result):        rows = []        sortedResult = self.sortResult(result.result)        for cid, (cls, cls_results) in enumerate(sortedResult):            # subtotal for a class            np = nf = ne = 0            for n,t,o,e in cls_results:                if n == 0: np += 1                elif n == 1: nf += 1                else: ne += 1
            # format class description            if cls.__module__ == "__main__":                name = cls.__name__            else:                name = "%s.%s" % (cls.__module__, cls.__name__)            doc = cls.__doc__ and cls.__doc__.split("\n")[0or ""            desc = doc and '%s: %s' % (name, doc) or name
            row = self.REPORT_CLASS_TMPL % dict(                style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',                desc = desc,                count = np+nf+ne,                Pass = np,                fail = nf,                error = ne,                cid = 'c%s' % (cid+1),            )            rows.append(row)
            for tid, (n,t,o,e) in enumerate(cls_results):                self._generate_report_test(rows, cid, tid, n, t, o, e)
        report = self.REPORT_TMPL % dict(            test_list = ''.join(rows),            count = str(result.success_count+result.failure_count+result.error_count),            Pass = str(result.success_count),            fail = str(result.failure_count),            error = str(result.error_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 'f') + '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
        # o and e should be byte string because they are collected from stdout and stderr?        if isinstance(o,str):            TODO: some problem with 'string_escape': it escape \n and mess up formating            # uo = unicode(o.encode('string_escape'))            uo = e# uo = o.decode('latin-1')        else:            uo = o        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  # ue = e.decode('latin-1')        else:            ue = e
        script = self.REPORT_TEST_OUTPUT_TMPL % dict(            id = tid,            output = saxutils.escape(uo+ue),        )
        row = tmpl % dict(            tid = tid,            Class = (n == 0 and 'hiddenRow' or 'none'),            style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),            desc = desc,            script = script,            status = self.STATUS[n],        )        rows.append(row)        if not has_output:            return
    def _generate_ending(self):        return self.ENDING_TMPL

############################################################################### Facilities for running tests from the command line##############################################################################
# Note: Reuse unittest.TestProgram to launch test. In the future we may# build our own launcher to support more specific command line# parameters like test title, CSS, etc.class TestProgram(unittest.TestProgram):    """    A variation of the unittest.TestProgram. Please refer to the base    class for command line parameters.    """    def runTests(self):        # Pick HTMLTestRunner as the default test runner.        # base class's testRunner parameter is not useful because it means        # we have to instantiate HTMLTestRunner before we know self.verbosity.        if self.testRunner is None:            self.testRunner = HTMLTestRunner(verbosity=self.verbosity)        unittest.TestProgram.runTests(self)
main = TestProgram
############################################################################### Executing this module from the command line##############################################################################
if __name__ == "__main__":    main(module=None)

HTMLTestRunner生成测试报告

目录结构

../HTMLTestRunner.py

../test.py

  • 实例

test.py文件

import unittestfrom ddt import ddt, data, unpackimport HTMLTestRunnerimport timeimport os@ddt # 代表这个测试类使用了数据驱动ddtclass TestCases(unittest.TestCase):    def setUp(self):        print("*******************************")        print("每一条case执行之前都会执行这个方法")    def tearDown(self):        print("每一条case执行之后都会执行这个方法\n")    @data("hello""123"" ")  # 代表传入的参数,一共三个参数,每次执行传一个值给value。    def test_testcases1(self, value):        print("这是一条测试用例case")        try:            self.assertTrue(value.isalpha())            print("test pass")        except Exception as e:            print("出错啦,错误结果是%s" % e)            print("test failed")            raise e    @data((11), (12), (11.0)) # 代表传入的参数,一共三个参数,每次传入两个值,下面两个value分别来接收两个值。    @unpack # 告诉我们的测试用例传入的是两个以上的值    def test_testcases2(self, value1, value2):        print("这是一条测试用例case")        try:            self.assertEqual(value1, value2)            print("test pass")        except Exception as e:            print("出错啦,错误结果是%s" % e)            print("test failed")            raise eif __name__ == "__main__":# unittest.main(verbosity=2)#构造测试集	suite = unittest.TestSuite()#按测试类添加	loader = unittest.TestLoader()	suite.addTest(loader.loadTestsFromTestCase(TestCases))	report_dir = "../java/"	BASE_DIR = os.path.dirname(os.path.abspath(__file__))print(BASE_DIR)	now = time.strftime("%Y-%m-%d %H-%M-%S")# print(now)	reportname = BASE_DIR + "/" + now + " Test report.html"# print(reportname)with open(reportname, "wb+"as file:	    runner = HTMLTestRunner.HTMLTestRunner(file, 2, title="Model test report",	                                           description="Hello testers! This is the description of Model test"	                                                       "report")	    runner.run(suite)	    

生成html文件


【声明】内容源于网络
0
0
橙子好甜
人生苦短,我用python --it测试狗
内容 30
粉丝 0
橙子好甜 人生苦短,我用python --it测试狗
总阅读32
粉丝0
内容30