当前位置: 首页 > article >正文

Unittest02|TestSuite、TestRunner、HTMLTestRunner、处理excel表数据、邮件接收测试结果

目录

八、测试套件TestSuite和测试运行器TestRunner

1、基本概念

2、创建和使用测试套件

3、 自动发现测试用例、创建测试套件、运行测试

4、生成html的测试报告:HTMLTestRunner

1️⃣导入HTMLTestRunner模块

2️⃣运行测试用例并生成html文件

九、unittest测试框架的设计与实现

1、对项目重构

2、处理excel表的测试数据

十、使用邮件接收测试结果


八、测试套件TestSuite和测试运行器TestRunner

  • 测试套件(Test Suite)是用于组织和管理多个测试用例(Test Case)的容器,它允许你将不同模块中多个测试用例集合在一起并一起执行。
  • 测试运行器(TestRunner),用于执行 TestSuite 中的所有测试用例,并将结果以文本的形式输出到控制台。
  • TestSuite 和 TextTestRunner 通常是配合使用的。TestSuite 组织和管理测试用例,TextTestRunner 执行这些测试用例并输出测试结果。

1、基本概念

  • 测试用例(Test Case)是指单个的测试方法,它验证了某个功能或行为是否符合预期。
  • 测试套件(Test Suite)是由多个测试用例组成的集合。它可以包含多个测试用例,也可以包含其他测试套件,从而形成一个嵌套的结构。
  • 测试运行器(TestRunner):TextTestRunner 会接收一个 TestSuite 对象并执行其中的所有测试用例。它会在控制台中输出详细的测试结果,包括每个测试用例的执行情况(通过、失败、错误等)。另外通过自定义 TestResult 类,TextTestRunner 也可以支持更详细或不同格式的输出。

2、创建和使用测试套件

在 unittest 中,测试套件主要通过 unittest.TestSuite 类来创建。你可以通过以下两种方式来创建和执行测试套件:①手动将测试用例添加到测试套件;②自动发现并添加测试用例

3、 自动发现测试用例、创建测试套件、运行测试

unittest 提供了一个 discover() 方法,可以自动发现符合特定命名规则的测试用例并将它们组织成一个测试套件。它会递归地查找指定目录下的所有测试模块并将它们自动加载到测试套件中。

在当前项目中创建main.py文件

可以看到当前目录有很多测试类
import unittest

# 自动发现当前目录下的所有测试模块并运行
suite =unittest.TestSuite()
case = unittest.defaultTestLoader.discover(start_dir=".",pattern="test*.py")

suite.addTest(case)

# 创建测试运行器,执行测试套件,是固定的使用方法
runner = unittest.TextTestRunner()
runner.run(suite)
  • discover() 方法unittest.defaultTestLoader.discover() 会从指定的 start_dir 目录开始递归查找所有符合 pattern 模式的测试文件。默认情况下,文件名应该以 test_ 开头,后面跟 .py 后缀。
  • 执行测试套件:通过 unittest.TextTestRunner() 执行自动发现的测试套件。

4、生成html的测试报告:HTMLTestRunner

unittest自带的测试报告是文本格式的,可读性不强,所以一般使用第三方模块来生成测试报告。

HTMLTestRunner 是 Python unittest 框架的一个第三方扩展,用于将测试结果以 HTML 格式输出,而不是标准的文本输出。它提供了一个漂亮且直观的报告,适用于需要以网页形式查看测试结果的场景。

1️⃣导入HTMLTestRunner模块

HTMLTestRunner模块下载不能通过pip安装,需要新建新建 HTMLTestRunner.py 文件,将网页内容复制到文件中即可。

"""
A TestRunner for use with the Python unit testing framework. It
generates a HTML report to show the result at a glance.

The simplest way to use this is to invoke its main method. E.g.

    import unittest
    import HTMLTestRunner

    ... define your tests ...

    if __name__ == '__main__':
        HTMLTestRunner.main()


For more customization options, instantiates a HTMLTestRunner object.
HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.

    # output to a file
    fp = file('my_report.html', 'wb')
    runner = HTMLTestRunner.HTMLTestRunner(
                stream=fp,
                title='My unit test',
                description='This demonstrates the report output by HTMLTestRunner.'
                )

    # Use an external stylesheet.
    # See the Template_mixin class for more customizable options
    runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'

    # run the test
    runner.run(my_test_suite)


------------------------------------------------------------------------
Copyright (c) 2004-2007, Wai Yip Tung
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.
* Neither the name Wai Yip Tung nor the names of its contributors may be
  used to endorse or promote products derived from this software without
  specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

# URL: http://tungwaiyip.info/software/HTMLTestRunner.html

__author__ = "Wai Yip Tung"
__version__ = "0.8.2"


"""
Change History

Version 0.8.2
* Show output inline instead of popup window (Viorel Lupu).

Version in 0.8.1
* Validated XHTML (Wolfgang Borgert).
* Added description of test classes and test cases.

Version in 0.8.0
* Define Template_mixin class for customization.
* Workaround a IE 6 bug that it does not treat <script> block as CDATA.

Version in 0.7.1
* Back port to Python 2.3 (Frank Horowitz).
* Fix missing scroll bars in detail log (Podi).
"""

# TODO: color stderr
# TODO: simplify javascript using ,ore than 1 class in the class attribute?

import datetime
import io
import sys
import time
import unittest
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)

    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: '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" οnfοcus='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 οnfοcus='this.blur();' οnclick="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, 'Time 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")[0] or ""
            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
        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
        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)

2️⃣运行测试用例并生成html文件

HTMLTestRunner主要有三个参数:

  • stream:测试报告写入文件的存储位置。
  • title:测试报告的标题。
  • description:测试报告的描述。
import unittest
from HTMLTestRunner import HTMLTestRunner
# 自动发现当前目录下的所有测试模块并运行
suite =unittest.TestSuite()
case = unittest.defaultTestLoader.discover(start_dir=".",pattern="test*.py")

suite.addTest(case)

# 创建测试运行器,执行测试套件,是固定的使用方法
with open("测试报告.html","wb") as f:
    runner = HTMLTestRunner(stream=f,title="测试报告",description="这是项目的描述。。。")
    runner.run(suite)

九、unittest测试框架的设计与实现

1、对项目重构

  • 新建文件夹reports专门存放测试报告
  • 新建文件夹data专门存放测试数据
  • 新建文件夹case专门存放测试用例
  • 新建文件夹utils专门第三方的包等工具
  • 把utils中的HTMLTestRunner.py变成一个包:在utils文件夹中新建一个__init__.py 的空文件。这个文件告诉 Python 该目录应该被视为一个包。
  • 修改代码中start_dir的值.为case,即case = unittest.defaultTestLoader.discover(start_dir="case",pattern="test*.py")

  • 修改代码中测试报告的路径为:with open("reports/测试报告.html", "wb") as f:

  • 改造测试报告的名字,以便可以保存历史报告:首先引入time模块,获取现在的时间(年-月-日-时-分-秒),设置reportpath变量,替换测试报告的存放路径。

重构后的文件结构

修改后的main.py文件

import unittest
import time
from utils.HTMLTestRunner import HTMLTestRunner
# 自动发现当前目录下的所有测试模块并运行
suite =unittest.TestSuite()
case = unittest.defaultTestLoader.discover(start_dir="case",pattern="test*.py")

suite.addTest(case)

now = time.strftime("%Y-%m-%d-%H-%M-%S")
reportPath = f"reports/测试报告-{now}.html"

# 创建测试运行器,执行测试套件,是固定的使用方法
with open(reportPath, "wb") as f:
    runner = HTMLTestRunner(stream=f,title="测试报告",description="这是项目的描述。。。")
    runner.run(suite)

 2、处理excel表的测试数据

  • 下载第三方的包openpyxl
pip install openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple
  • 在utils中封装一个excelFunc.py的包,专门用来读excel表的数据
from openpyxl import load_workbook


def readExcleData(fileName,sheetName,coor):
    """
    读取excle的数据
    """
    excle = load_workbook(fileName)
    sheet = excle[sheetName]
    min_row = coor.get("min_row")
    max_row = coor.get("max_row")
    min_col = coor.get("min_col")
    max_col = coor.get("max_col")
    dataList = sheet.iter_rows(min_row,max_row,min_col,max_col)
    result = []
    for row in dataList:
        rows = []
        for i in row:
            rows.append(i.value)
        result.append(rows)
    return result


if __name__ == "__main__":
    coor = {
        "min_row":1, # 根据需求修改数字
        "max_row":1,
        "min_col":1,
        "max_col":1
    }
    data = readExcleData("../data/测试数据.xlsx","Sheet1",coor)
    print(data)

如果excel表中是:

读取2到4行,1到4列的结果

这样在做数据驱动测试的时候就可以使用ddt来处理测试数据了

十、使用邮件接收测试结果

  • 准备工作:在邮箱的设置中开启amtp功能,然后生成一个授权码 
以qq邮箱为例子
  • 在utils中封装一个emailFunc.py的包,实现发送邮件的功能
import smtplib
from email.mime.application import MIMEApplication
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

def sendEmail(receiver,filename):


    sender = 'xxxxxxx@qq.com'  # 改成发送人的具体邮箱,必须是有效的,不然会发送失败
    passwd = 'abcdxxxxxxx'     # 不是邮箱密码,而是授权码


    content = MIMEMultipart()

    content['From'] = sender
    content['To'] = receiver
    content['Subject'] = 'Python SMTP 邮件测试'

    # 添加文本内容
    text = MIMEText('测试报告已出,详情见附件', 'plain', 'utf-8')
    content.attach(text)

    # 添加附件
    with open(filename, 'rb') as f:
        attachment = MIMEApplication(f.read())  # 读取为附件
        attachment.add_header('Content-Disposition', 'attachment', filename='测试报告.html')
        content.attach(attachment)


    try:
        # 建立 SMTP 、SSL 的连接,连接发送方的邮箱服务器
        smtp = smtplib.SMTP_SSL('smtp.qq.com', 465)

        # 登录发送方的邮箱账号
        smtp.login(sender, passwd)

        # 发送邮件 发送方,接收方,发送的内容
        smtp.sendmail(sender, receiver, content.as_string())

        print('邮件发送成功')

        smtp.quit()
    except Exception as e:
        print(e)
        print('发送邮件失败')

  • 增加main.py的内容:添加了emailFunc包,以及最后一行调用endEmail代码
import unittest
import time
from utils.HTMLTestRunner import HTMLTestRunner
from utils.emailFunc import sendEmail
# 自动发现当前目录下的所有测试模块并运行
suite =unittest.TestSuite()
case = unittest.defaultTestLoader.discover(start_dir="case",pattern="test*.py")

suite.addTest(case)

now = time.strftime("%Y-%m-%d-%H-%M-%S")
reportPath = f"reports/测试报告-{now}.html"

# 创建测试运行器,执行测试套件,是固定的使用方法
with open(reportPath, "wb") as f:
    runner = HTMLTestRunner(stream=f,title="测试报告",description="这是项目的描述。。。")
    runner.run(suite)

# 测试报告以邮件发送给xxxxxxx@qq.com
sendEmail('xxxxxxx@qq.com',reportPath)

代码可参考:python 学习笔记(5)——SMTP 使用QQ邮箱发送邮件_qq邮箱smtp-CSDN博客

 全部内容:
Unittest01|TestCase、断言、装饰器、夹具、清理函数、ddt-CSDN博客
Unittest02|TestSuite、TestRunner、HTMLTestRunner、处理excel表数据、邮件接收测试结果-CSDN博客


http://www.kler.cn/a/451086.html

相关文章:

  • Mono里运行C#脚本3—mono_jit_init
  • 12寸半导体厂等保安全的设计思路
  • 32. 线程、进程与协程
  • C#(委托)
  • Leetcode 3393. Count Paths With the Given XOR Value
  • 03.01、三合一
  • Java爬虫获取1688 item_search_img接口详细解析
  • openjdk17 从C++视角看 String的intern的jni方法JVM_InternString方法被gcc编译器连接
  • 【逆向篇】Web逆向WebPack结构分析
  • 阿里云 Java 后端一面,什么难度?
  • 第11周作业
  • 全国硕士研究生入学考试(考研)常识详解之分数构成:初试成绩、复试成绩及复录比
  • C++练习:图论的两种遍历方式
  • 无人直播源码
  • 管理面板Ajenti的在Windows10下Ubuntu24.04/Ubuntu22.04里的配置管理
  • Redis的主从集群以及哨兵机制学习总结
  • Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
  • 单片机 STM32入门
  • windows C#-对象和集合初始值设定项(中)
  • RustDesk远程及自建服务器搭建教程
  • Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
  • 国标GB28181设备管理软件EasyGBS:P2P远程访问故障排查指南(设备端)
  • 自然语言处理与知识图谱的融合与应用
  • K8s - openeuler2203SP1安装 K8s + flannel
  • 浅谈 前端验证码那些事
  • STM32 与 AS608 指纹模块的调试与应用