python unittest 框架

一 什么是测试框架

1、测试框架是测试开发过程中提取特定领域测试方法共性部分形成的体系结构;
2、测试框架的作用:在其基础上重用测试设计原则和测试经验,调整部分内容便可满足需求,可提高测试用例设计开发质量,降低成本,缩短时间;
3、测试框架不是一个现成可用的系统,是一个半成品,需要测试工程师基于它结合自己的测试对象知识转化成自己的测试用例;
4、测试框架是提供给测试人员开发相应领域测试用例的测试分析设计工具;
5、测试框架不是测试用例集,而是通用的,具有一般性的系统主体部分。测试人员像做填空一样,根据具体业务完成特定应用系统中与众不同的特殊部分;
6、测试设计模式的思想(等价类/边界值)在测试框架中进行应用。

二 unitest的基本架构

import unittest

class TestDemoCls(unittest.TestCase):

    @classmethod
    def setUpClass(cls) -> None:
        print("类的初始化")

    @classmethod
    def tearDownClass(cls) -> None:
        print("类的释放")

    def setUp(self) -> None:
        print("用例的初始化")

    def tearDown(self) -> None:
        print("用例的释放")

    def test_1(self):
        print("用例执行在这里")

def setUpModule():
    print("集成测试 >>>>>>>>>>>>>>开始")

def tearDownModule():
    print("集成测试 >>>>>>>>>>>>>>结束")
    
if __name__ == '__main__':
    unittest.main(verbosity=2)

输出

# 集成测试 >>>>>>>>>>>>>>开始
# 类的初始化
# 用例的初始化
# 用例执行在这里
# 用例的结束
# 类的结束
# 集成测试 >>>>>>>>>>>>>>结束

首先,需要导入模块 unittest.创建一个测试类,继承unitest.TestCase
其次,1 setUpClass tearDownClass 是类的初始化和释放,通常情况下(一般情况下用不到,根据需求添加),一个类代表一个产品的一个功能模块,使用产品的功能用例模块化,书写清晰,代码整洁,代码执行的效果:每启动一次脚本只执行一次。
2 setUp tearDown 是用例的初始化和释放,执行的效果:每执行一次用例,该函数就执行一次。是针对用例的前置和后置条件的函数。
3 test_1 使用用例执行的函数。 用例的函数名必须使用test开头 顺序按照 0-9 a-z
4 setUpModule tearDownModule 和setUpClass tearDownClass 的效果一样, 一般情况很少用到
最后:执行 unittest.main(verbosity=2) 单元测试的入口 verbosity=2 可以不写, 表示输出较为详细的内容
5 setUpModule 和 tearDownModule 如果需要跨文件使用需要提前导包

Tips: 在终端执行测试框架
1 unittest 模块可以通过命令行运行模块、类和独立测试方法的测试:
定义 : 脚本文件 test1.py 用例的类 TestDemoCls 用例函数 :test_1
在终端的命令
python -m unittest test1.TestDemoCls.test_1
在这里插入图片描述
可以指定多个模块(模块就是一个.py文件) 后面可以加类名或用例的函数名
同样的,测试模块可以通过文件路径指定:
在这里插入图片描述
在这里插入图片描述
-v 输出的信息较为详细 对比上下两张图
在这里插入图片描述
当运行时不包含参数 开始探索性测试
注意:该命令执行探索当前目录下的测试脚本在这里插入图片描述
用于获取命令行选项列表: python -m unittest -h
在这里插入图片描述
在这里插入图片描述
-k
只运行匹配模式或子串的测试方法和类。可以多次使用这个选项,以便包含匹配子串的所有测试用例。

包含通配符(*)的模式使用 fnmatch.fnmatchcase() 对测试名称进行匹配。另外,该匹配是大小写敏感的。

模式对测试加载器导入的测试方法全名进行匹配。

例如,-k foo 可以匹配到 foo_tests.SomeTest.test_something 和 bar_tests.SomeTest.test_foo ,但是不能匹配到 bar_tests.FooTest.test_something 。

-f, --failfast
当出现第一个错误或者失败时,停止运行测试。
-p, --pattern pattern
用于匹配测试文件的模式(默认为 test*.py )。

三 unitest 测试套件

用例的一些小控件
@unittest.skip(原因)
跳过被此装饰器装饰的测试。 reason 为测试被跳过的原因。
@unittest.skipIf(条件,原因)
当 condition 为真时,跳过被装饰的测试。
@unittest.skipUnless(条件,原因)
跳过被装饰的测试,除非 condition 为真。
@unittest.expectedFailure
把测试标记为预计失败。如果测试不通过,会被认为测试成功;如果测试通过了,则被认为是测试失败。
上面的内容是针对某些情况做的处理

测试套件


import unittest
 
# 逐个加载测试类
test_1 = unittest.TestLoader().loadTestsFromTestCase({{测试用例的类名1}}) 
test_2 = unittest.TestLoader().loadTestsFromTestCase({{测试用例的类名2}})
test_3 = unittest.TestLoader().loadTestsFromTestCase({{测试用例的类名3}})
# 创建测试套件并添加测试类
s = unittest.TestSuite([test_1, test_2, test_3])
# 创建执行者
runner = unittest.TextTestRunner()
# 运行套件
runner.run(s)

四 数据驱动

配置环境 pip install ddt
导入模块 from ddt import ddt,data,unpack

import unittest
from ddt import ddt, data, unpack

test_data_list = [1, 2, 3, 4]
test_data_dict = [
    {"name": "tom", "age": 1},
 {"name": "marry", "age": 2},
 {"name": "jack", "age": 3}
]

@ddt()
class TestDemo(unittest.TestCase):

    def setUp(self) -> None:
        print("用例初始化~~~")
        
    def tearDown(self) -> None:
        print("用例的释放~~~")

    @data(*test_data_list)
    def test_0(self, v):
        print(v)
        
    @data(*test_data_dict)
    @unpack
    def test_1(self, name, age):
        print(name)
        print(age)
if __name__ == '__main__':
    unittest.main()

五 实例

在工作中的实际情况,不可能是直接使用继承 unittest.TestCase 的方式 写自动化脚本,这样的做不利于维护和区分脚本的使用范围,通常都是写一个base_module.py, 将一些通用的组件放在 《父类》中,例如前置条件, 后置条件 ,项目组logo, 解析json/yaml文件,连接数据库,连接服务器等操作,在实际运行《子类》中继承父类后,采用调用父类方法或重写父类方法达到实际的测试目的

# base.py
import unittest


class BaseModule(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        '''
        场景的前置条件
        '''
        cls.read_data(cls)
        cls.conn_mysql(cls)
        cls.logo(cls)
        print('前置操作完毕')
        cls.public_data_pool = dict() # 初始化一个资源池,通过key方便取值

    @classmethod
    def tearDownClass(cls) -> None:
        '''
        场景的后置处理
        '''
        print('场景的后置处理')

    def setUp(self) -> None:
        '''
        单条用例前置操作
        '''
        print('单条用例前置操作')

    def tearDown(self) -> None:
        '''
        单条用例的后置操作
        '''
        print('单条用例的后置操作')

    def conn_mysql(self):
        '''
        连接数据库
        '''
        print('连接数据库')

    def read_data(self):
        '''
        读取json/yaml文件
        '''
        print('读取json/yaml文件')

    def logo(self):
        '''
        logo 图
        '''
        print('logo 图')

# main.py

from base import * 

class OnlineShoppingModule(BaseModule):
    '''
    网购示例
    '''
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        print('子类自己的一些条件')

    def test_1(self):
        print('登录')
        self.public_data_pool['token'] = 'token值'

    def test_2(self):
        if self.public_data_pool.get('token') is not None:
            print('选择商品')


    def test_3(self):
        if self.public_data_pool.get('token') is not None:
            print('下单')
    
    def test_4(self):
        if self.public_data_pool.get('token') is not None:
            print('下单成功,请等待发货')
            

输出

# python3 -m unittest main
读取json/yaml文件
连接数据库
logo 图
前置操作完毕
子类自己的一些条件
单条用例前置操作
登录
单条用例的后置操作
.单条用例前置操作
选择商品
单条用例的后置操作
.单条用例前置操作
下单
单条用例的后置操作
.单条用例前置操作
下单成功,请等待发货
单条用例的后置操作
.场景的后置处理

----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK

工作还会遇到一些批量执行用例的情况,使用discover 来实现
目录结构
在这里插入图片描述

# base.py
import unittest


class BaseModule(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        '''
        场景的前置条件
        '''
        cls.read_data(cls)
        cls.conn_mysql(cls)
        cls.logo(cls)
        print('前置操作完毕')
        cls.public_data_pool = dict() # 初始化一个资源池,通过key方便取值

    @classmethod
    def tearDownClass(cls) -> None:
        '''
        场景的后置处理
        '''
        print('场景的后置处理')

    def setUp(self) -> None:
        '''
        单条用例前置操作
        '''
        print('单条用例前置操作')

    def tearDown(self) -> None:
        '''
        单条用例的后置操作
        '''
        print('单条用例的后置操作')

    def conn_mysql(self):
        '''
        连接数据库
        '''
        print('连接数据库')

    def read_data(self):
        '''
        读取json/yaml文件
        '''
        print('读取json/yaml文件')

    def logo(self):
        '''
        logo 图
        '''
        print('logo 图')

# online_shopping_test.py

from base import *

class OnlineShoppingModule(BaseModule):
    '''
    网购示例
    '''
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        print('-' * 50)
        print('网购')

    def test_1(self):
        print('登录')
        self.public_data_pool['token'] = 'token值'

    def test_2(self):
        if self.public_data_pool.get('token') is not None:
            print('选择商品')


    def test_3(self):
        if self.public_data_pool.get('token') is not None:
            print('下单')
    
    def test_4(self):
        if self.public_data_pool.get('token') is not None:
            print('下单成功,请等待发货')
# play_computer_test.py

from base import *

class PlayComputerModule(BaseModule):
    '''
    网购示例
    '''
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        print('-' * 50)
        print('去网吧玩游戏')

    def test_1(self):
        self.public_data_pool['token'] = '已经成年的身份证在手了'
        print('打车去网吧')

    def test_2(self):
        if self.public_data_pool.get('token') is not None:
            print('刷身份证开机')


    def test_3(self):
        if self.public_data_pool.get('token') is not None:
            print('玩游戏中')
    
    def test_4(self):
        if self.public_data_pool.get('token') is not None:
            print('时间到了,离开网吧')
# main.py

import unittest
import os

test_script_path = os.path.join(os.getcwd(), 'script')

if os.path.isdir(test_script_path):
    discover = unittest.defaultTestLoader.discover(test_script_path, '*test.py') # 在指定的目录下匹配符合要求的文件名
    runner = unittest.TextTestRunner() # 创建Runner 对象 
    runner.run(discover) # 批量运行

输出

# python3 main.py

读取json/yaml文件
连接数据库
logo 图
前置操作完毕
--------------------------------------------------
网购
单条用例前置操作
登录
单条用例的后置操作
.单条用例前置操作
选择商品
单条用例的后置操作
.单条用例前置操作
下单
单条用例的后置操作
.单条用例前置操作
下单成功,请等待发货
单条用例的后置操作
.场景的后置处理
读取json/yaml文件
连接数据库
logo 图
前置操作完毕
--------------------------------------------------
去网吧玩游戏
单条用例前置操作
打车去网吧
单条用例的后置操作
.单条用例前置操作
刷身份证开机
单条用例的后置操作
.单条用例前置操作
玩游戏中
单条用例的后置操作
.单条用例前置操作
时间到了,离开网吧
单条用例的后置操作
.场景的后置处理

----------------------------------------------------------------------
Ran 8 tests in 0.000s

OK
通常情况下,企业中的自动化执行通常是执行单条某个场景或批量执行,绝大多数都是封装一些命令行执行的命令,这样比较简洁,方便执行。但是执行的前提都是场景用例之间互不影响(单接口用例之间也是如此)。

场景用例之间,接口的依赖关系较强,将重要的数据放到公共资源池中,方便上下文使用
单接口之间,接口的依赖关系较弱(只是相对场景的)在前置方法中提前mock完毕,将重要的数据放到公共资源池中,而后在使用

六 测试报告

将以下内容复制 到 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, '\nTimeElapsed: %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)

我的目录结构
在这里插入图片描述

# main.py

import unittest
import os

from script.HTMLTestRunner import HTMLTestRunner

test_script_path = os.path.join(os.getcwd(), 'script')
test_report_path = os.path.join(os.getcwd(), 'report')

if os.path.isdir(test_script_path) and os.path.isdir(test_report_path):
    report_dir = os.path.join(test_report_path, 'test.html')
    with open(report_dir, 'wb') as fp:
        suite = unittest.defaultTestLoader.discover(test_script_path, '*test.py') # 在指定的目录下匹配符合要求的文件名
        # suite = unittest.TestSuite()
        # suite.addTest(OnlineShoppingModule('test_1'))
        HTMLTestRunner(stream = fp, 
                       title = 'xx项目测试报告',
                       description = 'Mac1').run(suite)
                       

生产的测试报告

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值