接口测试框架设计
本章内容:
- 接口测试工具
- 接口测试框架设计
- 签到系统实例
- 本章发布会系统使用 django开发,作者工作项目的缩写版
一、接口测试工具
1、Postman
Postman是一款功能强大的网页调试与发送网页HTTP请求的Chrome插件;
下载安装需要墙上网,点击Chrome浏览器右上角菜单栏 更多工具-->扩展程序 搜索安装;
发送POST请求:http://192.168.10.7:8001/api/user_sign/
发送GET请求 http://192.168.10.7:8001/api/user_sign/eid=6
2、Jmeter测试工具
Apache JMeter是Apache组织开发的基于Java的压力测试工具,用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其它测试领域;
Jmeter官网地址 : http://jmeter.apache.org/
其它自行了解吧
二、接口测试框架设计
本节会开发一个自动化测试框架,在其中会涉及到Requests库、unittest单元测试框架,MySQL数据库的操作,以及HTMLTestRunner生成测试报告。
1、接口测试工具的不足
测试数据不可控制
接口测试本质是对数据的测试,调用接口输入一些数据,随后接口返回一些数据,验证接口返回数据的正确性 。
例如:用户查询接口,要求输入用户名username,返回用户的年龄、性别、邮箱、手机号等数据。在测试接口时传入username=zhangsan,首先数据库里一定要有一条zhangsan的数据,否则接口返回为空,
如果要断言接口返回值,如asser age = 25,那么一定预先确定参数的返回数据。要想接口测试用例可以正确执行并断言通过,必须要事先插入测试数据(username=zhangsan,age=26,phone=1380013800...),一般接口测试工具不具备数据插入功能,在用工具运行测试用例前必须先插入数据,这样接口测试就没有所谓的【自动化了】
无法测试加密接口
测试工具一大硬伤无法测试加密接口,例如md5、base64、AES等常见加密方式,又或者接口的参数需要使用时间戳。
扩展能力不足
例如:将测试结果生成HTML格式测试报告,将测试报告发送到指定邮箱,想对接口测试做定时任务,持续集成等这些需求都是工具难以实现的。
2、接口自动化测试设计
一般的接口工具测试过程:
- 接口工具调用被测试系统的接口(传参 username="lishi")。
- 系统接口根据传参(username="lishi")向正式数据库中查询数据。
- 将查询结果组装成一定格式的数据,并返回给被调用者。
- 人工或通过工具的断言检查接口测试的正确性。
而设计接口自动化测试项目,为了使接口测试对数据变得可控,测试过程如下:
- 接口测试项目先向测试数据库插入测试数据(lishi的个人信息)。
- 调用被测试系统接口(传参username="lishi")。
- 系统接口根据传参(username="lishi")向测试数据库中进行查询并得到lishi个人信息。
- 将查询结果组装成一定格式的数据,并返回给被调用者。
- 通过单元测试框架断言接口返回的数据(lishi的个人信息),并生成测试报告。
为了使正式数据库的数据不被污染,建议使用独立的测试数据库。
三、Request库
Request是使用Apache2Licensed许可证的HTTP库,用Python编写。
Request使用的是urllib3,因此继承了它的所有特性,Requests支持HTTP连接保持和连接池,支持使用cookie保持会话,支持文件上传,支持自动确定相应内容的编码,支持国际化的URL 和 POST数据自动编码:
中文文档:http://cn.python-requests.org/zh_CN/latest/
官方网站:http://docs.python-requests.org/en/master/
1、安装
通过pip安装Requests
cmd
>>>Python\Scripts>pip3 install requests
2、接口测试
interface_test.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
import requests
import time
#查询发布会接口
url = "http://127.0.0.1:8001/api/get_event_list/"
r = requests.get(url,params={'eid':'1'})
result = r.json()
print("result返回的数据",result)
assert result['status'] == 200
assert result['message'] == "success"
assert result['data']['name'] == "iphone7s发布会"
assert result['data']['address'] == "广州会展中心"
assert result['data']['start_time'] == "2017-06-03T10:25:14Z"
PS : 因为”发布会查询接口“是GET类型,所以通过requests库的get()方法调用,第一个参数为调用接口的URL地址,params设置接口的数据,参数以字典形式组织。
json()方法可以将接口返回的json格式的数据转化为字典,接下来就是通过assert语句对字典中的数据进行断言,分别断言status/message/data的相关数据等。
3、接口自动化测试
使用unittest单元测试框架开发接口测试用例
import requests
import unittest
class GetEventListTest(unittest.TestCase):
""" 查询发布会接口测试"""
def setUp(self):
self.url = "http://127.0.0.1:8001/api/get_event_list/"
def test_get_event_null(self):
"""发布会id为空"""
r = requests.get(self.url,params={'eid':''})
result = r.json()
print(result)
self.assertEqual(result['status'],10021)
self.assertEqual(result['message'],"parameter_error")
def test_get_event_success(self):
"""发布会id为1,查询成功"""
r = requests.get(self.url,params={'eid':'1'})
result = r.json()
print(result)
self.assertEqual(result['status'],200)
self.assertEqual(result['message'],'success')
self.assertEqual(result['data']['name'],"iphone7s发布会")
self.assertEqual(result['data']['address'],"广州会展中心")
self.assertEqual(result['data']['start_time'],"2017-06-03T10:25:14Z")
if __name__ == '__main__':
unittest.main()
PS:显然用unittest单元测试框架组织和运行接口测试用例同样适用,这就是单元测试框架的核心和本质作用的提现。
四、接口自动化测试框架实现
接下来只需要集成数据库操作,以及HTMLTestRunner测试报告生成扩展即可。
4.1)、框架结构介绍
自动化测试框架目录结构如下:
py_request 框架
db_fixture --->初始化接口测试数据 ;
interface --->用于编写接口自动化测试用例;
report --->生成接口自动化测试报告;
db_config.ini --->数据库配置文件;
HTMLTestRunner.py unittest单元测试框架扩展,生成HTML格式的测试报告;
run_tests.py ---> 执行所有接口测试用例;
4.2)、数据库配置
首先创建数据库guest_test-->修改../guest/settings.py文件,生成测试数据库和测试用的表:
...
DATABASES = {
'default': {
'ENGINE':'django.db.backends.mysql',
'HOST' :'127.0.0.1',
'PORT' :'3306',
'NAME' :'guest_test',
'USER' :'tian',
'PASSWORD':'123456',
'OPTIONS':{
'init_command':"SET sql_mode='STRICT_TRANS_TABLES'",
},
}
}
...
PS: 修改数据库配置之后需重新执行 python manage.py migrate生成数据库表结构;
或者用数据库管理工具的导入和导出功能,将严格数据库的所有表结构导入到另一个数据库;
4.3)、完整框架实现
4.3.1) db_fixture目录下
db_fixture/test_data.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
import sys
sys.path.append('../db_fixture')
try:
from mysql_db import DB
except ImportError:
from .mysql_db import DB
#creat data 创建数据库
datas ={
'sign_event':[
{'id':1,'name':'红米发布会','`limit`':2000,'status':1,'address':'北京会展中心','start_time':'2017-08-20 14:00:00'},
{'id':2,'name':'我是来逗比的人数为0','`limit`':0,'status':1,'address':'广州白云区会展中心','start_time':'2017-08-21 14:00:00'},
{'id':3,'name':'当前状态为0关闭','`limit`':200,'status':0,'address':'广州琶洲会展中心','start_time':'2017-08-21 14:00:00'},
{'id':4,'name':'发布会已结束','`limit`':2000,'status':1,'address':'广州会展中心','start_time':'2001-08-20 14:00:00'},
{'id':5,'name':'小米5发布会','`limit`':2000,'status':1,'address':'深圳会展中心','start_time':'2017-08-20 14:00:00'},
],
'sign_guest':[
{'id':1,'realname':'alen','phone':13580423850,'email':'tian@mail.com','sign':0,'event_id':1},
{'id':2,'realname':'has sign','phone':13800138078,'email':'sign@mail.com','sign':1,'event_id':1},
{'id':3,'realname':'tom','phone':13680436580,'email':'tom@mail.com','sign':0,'event_id':5},
],
}
def init_data():
db=DB()
db.init_data(datas)
db_fixture/mysql_db.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
import pymysql.cursors
import os
class DB:
def __init__(self):
try:
#连接数据库
self.connection = pymysql.connect(host='127.0.0.1' ,
user='tian',
password='123456',
db ='guest_test',
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor)
except Exception as e:
print("Mysql Error %d : %s"%(e.args[0],e.args[1]))
#clear table data 清除数据库
def clear(self,table_name):
real_sql = "delete from %s;"%table_name
with self.connection.cursor() as cursor: #数据库光标
cursor.execute("SET FOREIGN_KEY_CHECKS=0;") #取消外键约束
cursor.execute(real_sql) #执行SQL语句
self.connection.commit() #写入数据库
#insert sql statement 插入数据
def insert(self,table_name,table_data):
for key in table_data:
table_data[key] ="'" + str(table_data[key])+"'"
key = ','.join(table_data.keys())
value = ','.join(table_data.values())
# real_sql = "INSERT INTO %s" + table_name + "("+key+") VALUES ("+value+")"
real_sql ="INSERT INTO %s(%s) VALUES (%s)"%(table_name,key,value)
with self.connection.cursor() as cursor: #打开光标
cursor.execute(real_sql) #执行sql语句
self.connection.commit() #写入数据库
#关闭数据库连接
def close(self):
self.connection.close()
#init data 初始化数据
def init_data(self,datas):
for table,data in datas.items():
self.clear(table)
for d in data:
self.insert(table,d)
self.close()
4.3.2 ) interface(测试用例集)
add_event_test.py 添加发布会
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
import unittest,requests,os,sys
path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(path)
from db_fixture import test_data
class AddEventTest(unittest.TestCase):
'''添加发布会'''
def setUp(self):
self.base_url ="http://127.0.0.1:8001/api/add_event/"
def tearDown(self):
print(self.result)
def test_add_event_all_null(self):
"""所有参数为空"""
payload ={'eid':'','':'','limit':'','address':'','start_time':''}
r = requests.post(self.base_url,data=payload)
self.result = r.json()
self.assertEqual(self.result['status'],10021)
self.assertEqual(self.result['message'],'parameter error')
def test_add_event_eid_exist(self):
"""id已经存在"""
payload = {'eid':1,'name':'番茄发布会','limit':2000,'address':'深圳宝安','start_time':'2017-6-12 12:25:00'}
r =requests.post(self.base_url,data=payload)
self.result = r.json()
self.assertEqual(self.result['status'],10022)
self.assertEqual(self.result['message'],'event id already exists')
def test_add_event_name_exist(self):
"""名称已经存在"""
payload = {'eid':11,'name':'红米发布会','limit':2000,'address':'广州白云','start_time':'2017'}
r = requests.post(self.base_url,data=payload)
self.result = r.json()
self.assertEqual(self.result['status'],10023)
self.assertEqual(self.result['message'],'event name already exists')
def test_add_event_data_type_error(self):
"""日期格式错误"""
payload ={'eid':11,'name':'地瓜发布会','limit':2000,'address':'广州白云','start_time':'2017'}
r =requests.post(self.base_url,data=payload)
self.result = r.json()
self.assertEqual(self.result['status'],10024)
self.assertIn('start_time format error',self.result['message'])
def test_add_event_success(self):
"""添加成功"""
payload ={'eid':11,'name':'玉米发布会','limit':2000,'address':'广州白云','start_time':'2017-07-10 12:00'}
r = requests.post(self.base_url,data=payload)
self.result = r.json()
self.assertEqual(self.result['status'],200)
self.assertEqual(self.result['message'],'add event success')
# if __name__ == '__main__':
#
# test_data.init_data() #初始化接口测试数据
# unittest.main()
add_guest_text.py 签到测试用例
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
import unittest,requests,os,sys
path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0,path)
from db_fixture import test_data
class AddGuessTest(unittest.TestCase):
""" 添加嘉宾"""
def setUp(self):
self.url = "http://127.0.0.1:8001/api/add_guest/"
def tearDown(self):
print(self.result)
def test_add_guest_all_null(self):
""" 参数为空"""
payload = {'eid':'','realname':'','phone':''}
r = requests.post(self.url,data=payload)
self.result = r.json()
self.assertEqual(self.result['status'],10021)
self.assertEqual(self.result['message'],'parameter_error')
def test_add_guest_eid_null(self):
''' eid = 901 查询id不存在'''
payload = {'eid':901,'realname':'jack','phone':13800138662}
r = requests.post(self.url,data=payload)
self.result = r.json()
self.assertEqual(self.result['status'],10022)
self.assertEqual(self.result['message'],'event id null')
def test_add_guest_status_close(self):
''' eid = 2 状态未开启'''
payload ={'eid':3,'realname':'tom','phone':13700137000}
r = requests.post(self.url,data=payload)
self.result = r.json()
self.assertEqual(self.result['status'],10023)
self.assertEqual(self.result['message'],'event status is not available')
def test_add_guest_limit_full(self):
'''eid=2 发布会人数已满'''
payload ={'eid':2,'realname':'jack','phone':13711001100}
r = requests.post(self.url,data=payload)
self.result = r.json()
self.assertEqual(self.result['status'],10024)
self.assertEqual(self.result['message'],'event number is full')
def test_add_guest_time_start(self):
''' eid = 4 发布会已开始 '''
payload = {'eid':4,'realname':'jack','phone':13800138000}
r = requests.post(self.url,data=payload)
self.result = r.json()
self.assertEqual(self.result['status'],10025)
self.assertEqual(self.result['message'],'event has started')
def test_add_guest_phone_repeat(self):
'''phone=13800138078手机号重复'''
payload ={'eid':1,'realname':'tom','phone':13800138078}
r = requests.post(self.url,data=payload)
self.result = r.json()
self.assertEqual(self.result['status'],10026)
self.assertEqual(self.result['message'],'the event guest phone number repeat')
def test_add_guest_success(self):
'''添加成功'''
payload = {'eid':1,'realname':'tom','phone':13583425632,'email':'tom@mail.com'}
r = requests.post(self.url,data=payload)
self.result = r.json()
self.assertEqual(self.result['status'],200)
self.assertEqual(self.result['message'],'add guest success')
get_event_list_test.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
import unittest,requests
import os,sys
path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0,path)
from db_fixture import test_data
class GetEventListTest(unittest.TestCase):
''' 获得发布会列表'''
def setUp(self):
self.url = "http://127.0.0.1:8001/api/get_event_list"
def tearDown(self):
print(self.result)
def test_get_event_list_eid_null(self):
'''查询为空'''
r = requests.get(self.url,params={'eid':''})
self.result = r.json()
self.assertEqual(self.result['status'],10021)
self.assertEqual(self.result['message'],'parameter_error')
def test_get_event_list_eid_error(self):
'''eid=901 查询id不存在'''
r = requests.get(self.url,params={'eid':901})
self.result =r.json()
self.assertEqual(self.result['status'],10022)
self.assertEqual(self.result['message'],"quer_result_is_empty")
def test_get_event_list_eid_success(self):
'''根据eid查询结果成功'''
r = requests.get(self.url,params={'eid':1})
self.result = r.json()
self.assertEqual(self.result['status'],200)
self.assertEqual(self.result['message'],'success')
self.assertEqual(self.result['data']['name'],'红米发布会')
self.assertEqual(self.result['data']['address'],'北京会展中心')
def test_get_event_list_nam_result(self):
'''关键字abc查询'''
r = requests.get(self.url,params={'name':'abc'})
self.result = r.json()
self.assertEqual(self.result['status'],10022)
self.assertEqual(self.result['message'],'query result is empty')
def test_get_event_list_name_find(self):
'''关键字发布会模糊查询'''
r =requests.get(self.url,params={'name':'发布会'})
self.result =r.json()
self.assertEqual(self.result['status'],200)
self.assertEqual(self.result['message'],'success')
self.assertEqual(self.result['data'][0]['name'],"红米发布会")
self.assertEqual(self.result['data'][0]['address'],'北京会展中心')
get_guest_list_test.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
import unittest,requests,os,sys
path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0,path)
class GetGuestListTest(unittest.TestCase):
'''获取嘉宾列表'''
def setUp(self):
self.url = "http://127.0.0.1:8001/api/get_guest_list/"
def tearDown(self):
print(self.result)
def test_get_guest_list_eid_null(self):
'''eid参数为空'''
r = requests.get(self.url,params={'eid':''})
self.result = r.json()
self.assertEqual(self.result['status'],10021)
self.assertEqual(self.result['message'],'eid cannot be empty')
def test_get_event_list_eid_error(self):
'''输入不存在eid 查询结果为不存在'''
r = requests.get(self.url,params={'eid':901})
self.result = r.json()
self.assertEqual(self.result['status'],10022)
self.assertEqual(self.result['message'],'query result is empty')
def test_get_event_list_eid_success(self):
'''根据eid查询结果成功'''
r = requests.get(self.url,params={'eid':1})
self.result = r.json()
self.assertEqual(self.result['status'],200)
self.assertEqual(self.result['message'],'success')
self.assertEqual(self.result['data'][0]['realname'],'alen')
self.assertEqual(self.result['data'][0]['phone'],'13580423850')
def test_get_event_list_eid_phone_success(self):
'''根据eid和phone查询结果成功'''
r = requests.get(self.url,params={'eid':1,'phone':'13580423850'})
self.result = r.json()
self.assertEqual(self.result['status'],200)
self.assertEqual(self.result['message'],'success')
self.assertEqual(self.result['data']['realname'],'alen')
self.assertEqual(self.result['data']['phone'],'13580423850')
py_request(主目录下)
run_tests.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
import time
from HTMLTestRunner import HTMLTestRunner
import unittest
from db_fixture import test_data
#指定测试用例为当前文件下的interface目录
test_dir = './interface'
#查找interface/目录下,所有匹配*_test.py的测试文件(*号匹配任意字符)
discover = unittest.defaultTestLoader.discover(test_dir,pattern='*_test.py')
if __name__ == '__main__':
test_data.init_data() #初始化接口测试数据
now = time.strftime("%Y-%m-%d %H-%M-%S")
filename ='./report/ {0} result.html'.format(now)
fp = open(filename,'wb') #打开文件
#运行所有测试用例且将运行结果写入
runner = HTMLTestRunner(stream=fp,title='2017年6月15日接口测试',
description='签到系统【内侧】自动化测试用例')
runner.run(discover)
fp.close()
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 , bugmaster"
__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"/>
<link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css">
<script src="http://cdn.bootcss.com/bootstrap/3.3.0/js/bootstrap.min.js"></script>
%(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)' class="btn btn-xs btn-primary">Summary</a>
<a href='javascript:showCase(1)' class="btn btn-xs btn-danger">Failed</a>
<a href='javascript:showCase(2)' class="btn btn-xs btn-info">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 class="text text-success">%(Pass)s</td>
<td class="text text-danger">%(fail)s</td>
<td class="text text-warning">%(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('.'+str(self.success_count))
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))
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
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)
运行成功后