UnitTest测试框架全栈详解(十)
从软件架构的⻆度来说,测试最重要的步骤是在软件开发的时候界入比较好,所以在早期测试的界入,从软件经济
学的⻆度上来说,发现的问题解决成本低,投入的资源比较少。因此,对一个测试的系统,开始最佳的测试就是源代码
级别的测试,也就是单元测试阶段,这个过程也被成为白盒测试。单元测试是最基本也是最底层的测试类型,单元测试
应用于最基本的软件代码,如类,函数。方法等,单元测试通过可执行的断言检查被测单元的输出是否满足预期结果。在
测试金字塔的理论上来说,越往下的测试投入资源越高,得到的回报率越大,⻅测试金字塔模型:
抛开软件架构的层面,在自动化测试的体系中,单元测试框架以及单元测试的知识体系是必须要掌握的技能之一, 单
元测试的知识体系是自动化测试工程师以及测试开发工程师的知识体系之一,而且是必须具备的知识之一。在 Python
语言中应用最广泛的单元测试框架是unittest和pytest,unittest属于标准库,只要安装了Python解释器后就 可以直接导
入使用了,pytest是第三方的库,需要单独的安装。单元测试框架的知识体系就围绕unittest和pytest来 讲解。
一、白盒测试原理
在软件架构的层面来说,测试最核心的步骤就是在软件开发过程中。就软件本身而言,软件的行为或者功能是软件
细节实现的产物,这些最终是交付给用户的东⻄。所以在早期执行测试的系统有可能是一个可测试和健壮的系统,
它会带来为用户提供的功能往往是让人满意的结果。因此给予这样的⻆度,开始执行测试的最佳方法是来自源代
码,也就是软件编写的地方以及开发人员。由于源代码是对开发人员是可⻅的,这样的一个测试过程我们可以称为
白盒测试。
二、自动化测试用例编写
不管基于什么的测试框架,自动化测试用例的编写都需要遵守如下的规则,具体总结如下:
三、UnitTest测试框架
3.1、UnitTest组件
unittest是属于Python语言的单元测试框架,它的核心组件具体可以总结为如下:
测试类继承unittest模块中的TestCase类后,依据继承的这个类来设置一个新的测试用例类和测试方法,案例代 码:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import unittest
class ApiTest(unittest.TestCase):
def test_001(self):
pass
3.1.1、测试固件
测试固件表示一个测试用例或者多个测试以及清理工作所需要的设置或者准备,⻅案例代码:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import unittest
class ApiTest(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
3.1.2、测试套件
测试套件顾名思义就是相关测试用例的集合。在unittest中主要通过TestSuite类提供对测试套件的支持,⻅案例带 代码:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import unittest
class ApiTest(unittest.TestCase):
def test_001(self):
pass
def test_002(self):
pass
if __name__ == '__main__':
suite=unittest.TestSuite()
suite.addTest('test_001')
unittest.TextTestRunner(verbosity=2).run(suite)
3.1.3、测试运行
管理和运行测试用例的对象。
3.1.4、测试断言
对所测试的对象依据返回的实际结果与期望结果进行断言校验
3.1.5 、测试结果
测试结果类管理着测试结果的输出,测试结果呈现给最终的用户来反馈本次测试执行的结果信息。
3.2、unittest测试固件详解
在unittest中测试固件依据方法可以分为两种执行方式,一种是测试固件只执行一次,另外一种是测试固件每次都
执行,下面依据具体的案例来讲解二者。
3.2.1、测试固件每次均执行
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import unittest
from selenium import webdriver
class ApiTest(unittest.TestCase):
def setUp(self):
self.driver=webdriver.Chrome()
self.driver.maximize_window()
self.driver.get('http://www.baidu.com')
self.driver.implicitly_wait(30)
def tearDown(self):
self.driver.quit()
def test_baidu_title(self):
self.assertEqual(self.driver.title,'百度一下,你就知道')
def test_baidu_url(self):
self.assertEqual(self.driver.current_url,'https://www.baidu.com/')
if __name__ == '__main__':
unittest.main(verbosity=2)
3.2.2、测试固件只执行一次
使用的是类方法,这样测试固件只会执行一次的,
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import unittest
from selenium import webdriver
class ApiTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.driver=webdriver.Chrome()
cls.driver.maximize_window()
cls.driver.get('http://www.baidu.com')
cls.driver.implicitly_wait(30)
@classmethod
def tearDownClass(cls):
cls.driver.quit()
def test_baidu_title(self):
self.assertEqual(self.driver.title,'百度一下,你就知道')
def test_baidu_url(self):
self.assertEqual(self.driver.current_url,'https://www.baidu.com/')
if __name__ == '__main__':
unittest.main(verbosity=2)
3.2.3 、测试用例执行顺序详解
在unittest中,测试点的执行顺序是依据ascill码来执行的,也就是说根据ASCII码的顺序加载,数字与字母的顺序
为:0-9,A-Z,a-z,所以以A开头的测试用例方法会优先执行,以a开头会后执行。也就是根据数字的大小从小到 大执
行的,切记数字的大小值的是不包含test,值的是test后面的测试点的数字大小,⻅案例代码:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import unittest
import unittest
class Api(unittest.TestCase):
def test_001(self):
pass
def test_002(self):
pass
if __name__ == '__main__':
unittest.main(verbosity=2)
执行的顺序为:test_001,下来时test_002 当然测试点不会单纯是数字的,也有字符串的,在python中,
字符串与数字的转换为:
chr():数字转为字母
ord():字母转为数字
⻅测试点的名称是字符串的执行顺序:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import unittest
class Api(unittest.TestCase):
def test_abc(self):
pass
def test_acb(self):
pass
if __name__ == '__main__':
unittest.main(verbosity=2)
备注:执行的顺序为:test_abc,下来是test_acb
再看字符串与数字的比较:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import unittest
class Api(unittest.TestCase):
def test_api_0a0(self):
pass
def test_api_00a(self):
pass
if __name__ == '__main__':
unittest.main(verbosity=2)
3.3、编写测试用例注意事项
1、在一个测试类里面,每一个测试方法都是以test开头的,test不能是中间或者尾部,必须是开头,建议test_
2、每一个测试用例方法都应该有注释信息,这样在测试报告就会显示具体的测试点的检查点
3、在自动化测试中,每个测试用例都必须得有断言,无断言的自动化测试用例是无效的
4、最好一个测试用例方法对应一个业务测试点,不要多个业务检查点写一个测试用例
5、如果涉及到业务逻辑的处理,最好把业务逻辑的处理方法放在断言前面,这样做的目的是不要因为业务逻辑执 行错误导致断言也是失败
6、测试用例名称最好规范,有约束
7、是否先写自动化测试的测试代码,在使用自动化测试方式写,本人觉得没必要,毕竟能够做自动化测试的都具
备了功能测试的基本水平,所以没必要把一个业务的检查点写多次,浪费时间和人力成本。
见案例代码:
def test_baidu_title(self):
'''验证:验证百度首⻚的title是否正确'''
self.assertEqual(self.driver.title,'百度一下,你就知道')
下面就以具体的案例演示第五点的情况,⻅测试的源码:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import requests
import unittest
import os
class ApiTest(unittest.TestCase):
def writeBookID(self,bookID):
with open(os.path.join(os.path.dirname(__file__),'bookID'),'w') as f:
f.write(str(bookID))
@property
def readBookID(self):
with open(os.path.join(os.path.dirname(__file__), 'bookID'), 'r') as f:
return f.read()
def creeteBook(self):
dict1={'author':'无涯','name':'Python自动化测试实战','done':True}
r=requests.post(url='http://localhost:5000/v1/api/books',json=dict1)
self.writeBookID(r.json()[0]['datas']['id'])
return r
def test_api_book(self):
'''API测试:查询书籍'''
self.creeteBook()
r=requests.get(url='http://localhost:5000/v1/api/book/{0}'.format(self.readBookID))
self.assertEqual(r.json()['datas'][0]['name'],'Python自动化测试实战')
self.assertEqual(r.json()['datas'][0]['id'],int(self.readBookID))
def delBook(self):
r=requests.delete(url='http://localhost:5000/v1/api/book/{0}'.format(self.readBookID))
if __name__ == '__main__':
unittest.main(verbosity=2)
3.4、测试套件详解
UnitTest的测试框架中提供了很丰富的测试套件,所谓测试套件其实我们可以把它理解为测试用例的集合,
或者可以说理解为一个容器,在这个容器里面可以存放很多的测试用例。下面详细的说明下各个不同测试套件
的应用和案例实战。
3.4.1、按测试类执行
按测试类执行,可以理解为在测试套件中,我们按测试类的方式来进行执行,那么也就不需要在乎一个
测试类里面有多少测试用例,我们是以测试类为单位来进行执行,测试类里面有多少的测试用例,我们都会
进行执行,下面详细的演示这部分的具体应用,具体案例代码如下:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import unittest
from selenium import webdriver
from test.init import Init
class Baidu(Init):
def test_baidu_shouye_title(self):
'''验证百度首页的title信息'''
assert self.driver.title=='百度一下,你就知道'
def test_baidu_shouye_url(self):
'''验证百度的首页URL地址'''
assert self.driver.current_url=='https://www.baidu.com/'
if __name__ == '__main__':
'''按测试类执行'''
suite=unittest.TestLoader().loadTestsFromTestCase(Baidu)
unittest.TextTestRunner(verbosity=2).run(suite)
执行如上的代码后,就会按类为单位来进行执行。
3.4.2、按测试模块执行
思维按测试模块来执行,就是以模块为单位来进行执行,那么其实在一个模块里面可以编写很多的类,
下面通过详细的代码演示下这部分,具体案例代码如下:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import unittest
from selenium import webdriver
from test.init import Init
class Baidu(Init):
def test_baidu_shouye_title(self):
'''验证百度首页的title信息'''
assert self.driver.title=='百度一下,你就知道'
def test_baidu_shouye_url(self):
'''验证百度的首页URL地址'''
assert self.driver.current_url=='https://www.baidu.com/'
class BaiDuSo(Init):
def test_baidu_so_value(self):
'''百度搜索关键字的验证'''
so=self.driver.find_element_by_id('kw')
so.send_keys('Selenium4')
assert so.get_attribute('value')=='Selenium4'
if __name__ == '__main__':
'''按测试类执行'''
suite=unittest.TestLoader().loadTestsFromModule('test_module.py')
unittest.TextTestRunner(verbosity=2).run(suite)
3.4.3、按具体的测试用例来执行
当然如果是仅仅执行某一个测试用例,执行的方式一种是鼠标放到具体的测试用例,然后右键执行
就可以了,另外一种方式是我们可以把需要执行的测试用例添加到测试套件中,然后来单独的进行执行,这
种方式其实我个人是不建议的,但是还是通过具体的代码来演示下,具体如下:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import unittest
from selenium import webdriver
from test.init import Init
class Baidu(Init):
def test_baidu_shouye_title(self):
'''验证百度首页的title信息'''
assert self.driver.title=='百度一下,你就知道'
def test_baidu_shouye_url(self):
'''验证百度的首页URL地址'''
assert self.driver.current_url=='https://www.baidu.com/'
if __name__ == '__main__':
'''按测试类执行'''
apiSuite=unittest.makeSuite(Baidu,'test_baidu_shouye_title')
suite=unittest.TestSuite(apiSuite)
unittest.TextTestRunner(verbosity=2).run(suite)
3.4.4、自定义测试套件
针对测试套件的方式是很多的,那么我们是否可以把加载所有测试用例的方式单独分离出来了,
当然其实是可以的,这样我们只需要关注更多的测试用例的执行,下面具体演示下测试套件的分离部
分,案例代码如下:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import unittest
from selenium import webdriver
from test.init import Init
class Baidu(Init):
def test_baidu_shouye_title(self):
'''验证百度首页的title信息'''
assert self.driver.title=='百度一下,你就知道'
def test_baidu_shouye_url(self):
'''验证百度的首页URL地址'''
assert self.driver.current_url=='https://www.baidu.com/'
def suite(self):
'''自定义测试套件'''
return unittest.TestLoader().loadTestsFromModule('test_customer.py')
if __name__ == '__main__':
'''自定义测试套件'''
unittest.TextTestRunner(verbosity=2).run(Baidu.suite())
3.4.5、分离测试套件
在一个完整的自动化测试用例中,比如在UI的自动化测试用例中,我们的测试用例是按照业务
模块来进行划分的,那么以为着我们需要编写很多的模块,但是就存在重复的代码,比如我们针对百
度产品的测试,不管是测试什么模块,测试固件这部分的代码每个测试模块都是一样的,这样就导致
很多的重复的代码,重复必然就带来测试效率的低下的问题,举一个很简单的问题,比如需要修改测
试的地址,就需要修改很多的测试模块,但是如果把测试套件分离出来,我们这需要修改一个地方就
可以了,这样我们的测试效率就提升了一点,毕竟效率的提升是需要做很多的,不可能一点就进行大
幅度的提升的。分离测试套件的思想其实很简单的,就是使用了继承的思想来解决这个问题,我们把
测试固件分离到init.py里面,代码具体如下:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import unittest
from selenium import webdriver
class Init(unittest.TestCase):
def setUp(self) -> None:
self.driver=webdriver.Chrome()
self.driver.maximize_window()
self.driver.implicitly_wait(30)
self.driver.get('http://www.baidu.com')
def tearDown(self) -> None:
self.driver.quit()
这样其他测试模块就需要引人这个模块中的Init类就可以了,然后再继承这个类,具体的代码如下:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import unittest
from selenium import webdriver
from test.init import Init
class Baidu(Init):
def test_baidu_shouye_title(self):
'''验证百度首页的title信息'''
assert self.driver.title=='百度一下,你就知道'
def test_baidu_shouye_url(self):
'''验证百度的首页URL地址'''
assert self.driver.current_url=='https://www.baidu.com/'
if __name__ == '__main__':
'''按测试类执行'''
suite=unittest.TestLoader().loadTestsFromTestCase(Baidu)
unittest.TextTestRunner(verbosity=2).run(suite)
其实过程我们使用了很简单的思维,但是解决了一个很核心的问题。
3.5、unittest之参数化
在unittest的测试框架中,可以结合ddt的模块来达到参数化的应用,当然关于ddt库的应用在数据驱动
方面有很详 细的解释,这里就直接说另外的一个第三方的库parameterized,安装的命令为:
pip3 install parameterized
安装成功后,这里就以一个两个数相加的案例来演示它的应用,实现的源码如下:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
'''
本质思想:把测试的数据看成列表当中的一个元素,
那么针对列表进行循环的时候,把每个元素进行赋值
'''
def add(a,b):
return a+b
import unittest
from parameterized import parameterized,param
class AddTest(unittest.TestCase):
@parameterized.expand([
param(1,1,2),
param('a','b','ab')
])
def test_add_function(self,x,y,result):
print(result)
assert add(a=x,b=y)==result
if __name__ == '__main__':
unittest.main(verbosity=2)
在UI自动化测试中,parameterized也是特别的有用,如针对一个登录案例的测试,针对登录就会有很多
的测试案 例的,主要是用户名和密码的input表单的验证以及错误信息的验证,下面就结合具体的案例来看
它在UI自动化测 试中的应用,案例源码为:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
'''
本质思想:把测试的数据看成列表当中的一个元素,
那么针对列表进行循环的时候,把每个元素进行赋值
'''
def add(a,b):
return a+b
import unittest
from parameterized import parameterized,param
from selenium import webdriver
import time as t
class AddTest(unittest.TestCase):
def setUp(self) -> None:
self.driver=webdriver.Chrome()
self.driver.maximize_window()
self.driver.implicitly_wait(30)
self.driver.get('https://mail.sina.com.cn/#')
def tearDown(self) -> None:
self.driver.quit()
@parameterized.expand([
param('','','请输入邮箱名'),
param('srtSA','saert','您输入的邮箱名格式不正确'),
param('aserSDAsd@sina.com','asdfrty','登录名或密码错误')
])
def test_sina_email(self,username,password,result):
t.sleep(2)
self.driver.find_element_by_id('freename').send_keys(username)
t.sleep(2)
self.driver.find_element_by_id('freepassword').send_keys(password)
t.sleep(2)
self.driver.find_element_by_link_text('登录').click()
t.sleep(3)
div=self.driver.find_element_by_xpath('/html/body/div[3]/div/div[2]/div/div/div[4]/div[1]/div[1]/div[1]/span[1]')
assert div.text==result
if __name__ == '__main__':
unittest.main(verbosity=2)
3.7、测试报告
下面详细的说明下测试报告的生成以及加载所有测试模块的过程,我们在tests的模块下
编写了很多的测试用例,但是实际的生产环境中总不能按测试模块来执行,我们都是加载所有的
测试模块来执行并且最终生成基于HTML的测试报告,测试报告会使用到第三方的库
HTMLTestRunner,下载的地址为:https://github.com/tungwaiyip/HTMLTestRunner,当然针对
Python3需要修改下源码部分,整体源码为:
"""
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
__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,'&');
s = s.replace(/</g,'<');
s = s.replace(/>/g,'>');
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> </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'> </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")[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 = o.decode('latin-1')
uo = o
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.decode('latin-1')
ue = e
else:
ue = e
script = self.REPORT_TEST_OUTPUT_TMPL % dict(
id = tid,
output = saxutils.escape(str(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)
把该模块需要放在 /Library/Frameworks/Python.framework/Versions/3.7/lib的目录下。
3.7.1、加载所有的测试模块
下面我们编写具体的函数来加载所有的测试模块,路径处理部分我们使用os的模块来进行处理,针对
路径处理这部分特别的再说下,不能使用硬编码,使用硬编码只会带来维护的成本性,而且也涉及到不同的
操作系统针对路径是有不同的,比如MAC和Linux下是没有C盘的,但是Windows操作系统是有的,这部分需
要特别的注意下,下面的函数主要体现的是加载所有测试模块的代码,具体如下:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import os
import HTMLTestRunner
import unittest
def base_dir():
return os.path.join(os.path.dirname(__file__),'test')
def getSuite():
tests=unittest.defaultTestLoader.discover(
start_dir=base_dir(),
pattern='test_*.py',
top_level_dir=None
)
return tests
下面我们调用getSuite的方法,调用的代码为:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import os
import HTMLTestRunner
import unittest
def base_dir():
return os.path.join(os.path.dirname(__file__),'test')
def getSuite():
tests=unittest.defaultTestLoader.discover(
start_dir=base_dir(),
pattern='test_*.py',
top_level_dir=None
)
return tests
if __name__ == '__main__':
for item in getSuite():
print(item)
见如下输出的结果信息:
<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<test_assert.Baidu testMethod=test_baidu_shouye_so>, <test_assert.Baidu testMethod=test_baidu_shouye_title>, <test_assert.Baidu testMethod=test_baidu_shouye_url>]>, <unittest.suite.TestSuite tests=[]>]>
<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<test_baidu_shouYe.Baidu testMethod=test_baidu_shouye_title>, <test_baidu_shouYe.Baidu testMethod=test_baidu_shouye_url>]>, <unittest.suite.TestSuite tests=[]>]>
<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<test_customer.Baidu testMethod=test_baidu_shouye_title>, <test_customer.Baidu testMethod=test_baidu_shouye_url>]>, <unittest.suite.TestSuite tests=[]>]>
<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<test_make_suite.Baidu testMethod=test_baidu_shouye_title>, <test_make_suite.Baidu testMethod=test_baidu_shouye_url>]>, <unittest.suite.TestSuite tests=[]>]>
<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<test_module.BaiDuSo testMethod=test_baidu_so_value>]>, <unittest.suite.TestSuite tests=[<test_module.Baidu testMethod=test_baidu_shouye_title>, <test_module.Baidu testMethod=test_baidu_shouye_url>]>, <unittest.suite.TestSuite tests=[]>]>
<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<test_params.AddTest testMethod=test_add_function_0>, <test_params.AddTest testMethod=test_add_function_1_a>]>]>
<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<test_sina.AddTest testMethod=test_sina_email_0_>, <test_sina.AddTest testMethod=test_sina_email_1_srtSA>, <test_sina.AddTest testMethod=test_sina_email_2_aserSDAsd_sina_com>]>]>
Process finished with exit code 0
从如上的输出结果来看,已经到模块级别了,下面我们出一个需求,总共有多少测试用例,请统计出来,
其实我们可以针对模块进行再次进行循环到类级别,然后到测试用例的级别,然后把所有的测试用例加
到一个列表里面,获取列表的长度就是测试用例的总数,下面是到类级别的代码和输出结果:
if __name__ == '__main__':
for item in getSuite():
for i in item:
print(i)
输出结果为:
<unittest.suite.TestSuite tests=[<test_assert.Baidu testMethod=test_baidu_shouye_so>, <test_assert.Baidu testMethod=test_baidu_shouye_title>, <test_assert.Baidu testMethod=test_baidu_shouye_url>]>
<unittest.suite.TestSuite tests=[]>
<unittest.suite.TestSuite tests=[<test_baidu_shouYe.Baidu testMethod=test_baidu_shouye_title>, <test_baidu_shouYe.Baidu testMethod=test_baidu_shouye_url>]>
<unittest.suite.TestSuite tests=[]>
<unittest.suite.TestSuite tests=[<test_customer.Baidu testMethod=test_baidu_shouye_title>, <test_customer.Baidu testMethod=test_baidu_shouye_url>]>
<unittest.suite.TestSuite tests=[]>
<unittest.suite.TestSuite tests=[<test_make_suite.Baidu testMethod=test_baidu_shouye_title>, <test_make_suite.Baidu testMethod=test_baidu_shouye_url>]>
<unittest.suite.TestSuite tests=[]>
<unittest.suite.TestSuite tests=[<test_module.BaiDuSo testMethod=test_baidu_so_value>]>
<unittest.suite.TestSuite tests=[<test_module.Baidu testMethod=test_baidu_shouye_title>, <test_module.Baidu testMethod=test_baidu_shouye_url>]>
<unittest.suite.TestSuite tests=[]>
<unittest.suite.TestSuite tests=[<test_params.AddTest testMethod=test_add_function_0>, <test_params.AddTest testMethod=test_add_function_1_a>]>
<unittest.suite.TestSuite tests=[<test_sina.AddTest testMethod=test_sina_email_0_>, <test_sina.AddTest testMethod=test_sina_email_1_srtSA>, <test_sina.AddTest testMethod=test_sina_email_2_aserSDAsd_sina_com>]>
Process finished with exit code 0
再循环一层,到具体的测试用例级别,代码为:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import os
import HTMLTestRunner
import unittest
def base_dir():
return os.path.join(os.path.dirname(__file__),'test')
def getSuite():
tests=unittest.defaultTestLoader.discover(
start_dir=base_dir(),
pattern='test_*.py',
top_level_dir=None
)
return tests
if __name__ == '__main__':
for item in getSuite():
for i in item:
for j in i:
print(j)
输出结果信息为具体的测试用例,具体如下:
test_baidu_shouye_so (test_assert.Baidu)
test_baidu_shouye_title (test_assert.Baidu)
test_baidu_shouye_url (test_assert.Baidu)
test_baidu_shouye_title (test_baidu_shouYe.Baidu)
test_baidu_shouye_url (test_baidu_shouYe.Baidu)
test_baidu_shouye_title (test_customer.Baidu)
test_baidu_shouye_url (test_customer.Baidu)
test_baidu_shouye_title (test_make_suite.Baidu)
test_baidu_shouye_url (test_make_suite.Baidu)
test_baidu_so_value (test_module.BaiDuSo)
test_baidu_shouye_title (test_module.Baidu)
test_baidu_shouye_url (test_module.Baidu)
test_add_function_0 (test_params.AddTest)
test_add_function_1_a (test_params.AddTest)
test_sina_email_0_ (test_sina.AddTest)
test_sina_email_1_srtSA (test_sina.AddTest)
test_sina_email_2_aserSDAsd_sina_com (test_sina.AddTest)
下面我们定义一个列表,然后获取测试用例的总数,具体代码如下:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import os
import HTMLTestRunner
import unittest
def base_dir():
return os.path.join(os.path.dirname(__file__),'test')
def getSuite():
tests=unittest.defaultTestLoader.discover(
start_dir=base_dir(),
pattern='test_*.py',
top_level_dir=None
)
return tests
if __name__ == '__main__':
testCases=list()
for item in getSuite():
for i in item:
for j in i:
testCases.append(j)
print('测试用例的总数为:{0}'.format(len(testCases)))
3.7.2、生成HTML测试报告
下面具体展示测试报告的生成,把测试报告存储到report的文件夹里面,思考到每次
生成的测试报名名称一直,我们可以以当前时间作为区分,那么整体实现的代码如下:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import os
import HTMLTestRunner
import unittest
import time
def base_dir():
return os.path.join(os.path.dirname(__file__),'test')
def getSuite():
tests=unittest.defaultTestLoader.discover(
start_dir=base_dir(),
pattern='test_*.py',
top_level_dir=None
)
return tests
def getNowTime():
return time.strftime('%y-%m-%d %h_%m_%s',time.localtime(time.time()))
def run():
filename=os.path.join(os.path.dirname(__file__),'report',getNowTime()+'report.html')
fp=open(filename,'wb')
runner=HTMLTestRunner.HTMLTestRunner(
stream=fp,
title='',
description=''
)
runner.run(getSuite())
if __name__ == '__main__':
run()
执行后,输出结果为:
.................<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>
Time Elapsed: 0:01:13.527186
在report的文件夹下,生成的测试报告打开后显示如下:
如上显示的是测试报告的详细信息,电锯Detail后,可以查看详细的信息。
感谢您的阅读,后续持续更新!您也可以加我微信互相学习!