unittest搭建項目目錄結構

casedata.py文件代碼
import pandas
from project_M1.common import log
def read_cases(excel, columns=[]): # 讀取excel用例中的指定列
excel = '../excelcase/' + excel
try:
if len(columns) == 0:
file = pandas.read_excel(excel) # 讀取所有列
else:
file = pandas.read_excel(excel, usecols=columns) # 讀取指定列
data = file.values.tolist() # 文件數據轉為列表
log.log().info('讀取用例文件' + excel + '成功')
return data
except Exception as e:
log.log().error('讀取測試用例文件出錯')
if __name__ == '__main__':
print(read_cases('login.xlsx'))
db.py文件代碼
from project_M1.common import entry, log
import configparser, os, pymysql
class DB:
def __init__(self):
"""
讀取db.conf文件,獲得數據庫服務器的信息
返回內容是 數據庫服務器信息
"""
try: # 異常處理
# 獲得測試要用的數據庫服務器的節點名
which_db = entry.Entry().get_which_db()
conf = configparser.ConfigParser()
conf.read('../conf/db.conf', encoding='utf-8')
host = conf.get(which_db, 'host')
port = conf.get(which_db, 'port')
user = conf.get(which_db, 'user')
password = conf.get(which_db, 'password')
db = conf.get(which_db, 'db')
self.dbinfo = {'host': host, 'port': int(port), "user": user, 'password': password, 'db': db}
log.log().info('數據庫信息==' + str(self.dbinfo))
except Exception as e:
log.log().error('數據庫配置文件[db.conf]讀取出錯' + e)
def conn_db(self): # 連接數據庫的函數
try:
# conn = pymysql.connect(host='192.168.175.128',port=3306,user='root',password='123456',db='exam')
conn = pymysql.connect(**self.dbinfo) # 連接數據庫
log.log().info('連接數據庫成功')
return conn
except Exception as e:
log.log().error('數據庫連接出錯' + e)
def get_sqlfiles(sqlf, sqlfiles=[]): # sqlfiles默認參數,要給參數只能給列表,要么不給
try:
if len(sqlfiles) == 0: # 表示沒有給實參,則讀取所有sql文件
sqlfiles = [file for file in os.listdir('../initsqls/') if
file.endswith('.sql')] # 所有.sql文件名存入sqlfiles列表
sqlfiles = ['../initsqls/' + i for i in sqlfiles]
log.log().info('獲得sql文件名' + str(sqlfiles) + '列表成功')
return sqlfiles
except Exception as e:
log.log().error('獲得sql文件名列表失敗')
# 讀取帶.sql文件擴展名文件中的sql語句
def read_sqls(self, sqlfiles=[]):
sqlfiles = self.get_sqlfiles(sqlfiles) # 獲得帶路徑的文件名列表
sqls = [] # 存sql語句的列表
try:
for sqlfile in sqlfiles: # sqlfile表示每一個sql語句文件
sfile = open(sqlfile, 'r', encoding='utf-8') # 打開文件
for sql in sfile: # sql是從sfile文件中獲得每一行
# 如果這行字符長度大於0 並且 不是以--開頭的
if len(sql.strip()) > 0 and not sql.startswith('--'):
sqls.append(sql.strip()) # sql語句中的回車不存入列表
log.log().info('讀取初始化sql語句成功==' + str(sqlfiles))
return sqls
except Exception as e:
log.log().error('讀取初始化sql語句出錯' + e)
def init_db(self, sqlfiles=[]): # #執行sqlfiles文件中的sql語句,初始化數據庫
"""
目的:數據初始化
:param sqlfiles: 指定sql文件
:return:
"""
sqls = self.read_sqls(sqlfiles) # 讀取所有需要執行的sql命令
conn = self.conn_db() # 連接數據庫
cursor = conn.cursor() # 創建游標
try:
for sql in sqls:
cursor.execute(sql) # 只能一條一條的執行,結果暫存到cursor中
conn.commit() # 提交數據到數據庫
conn.close()
log.log().info('初始化數據庫成功')
except Exception as e:
log.log().error('初始化數據庫出錯')
def check_db(self, case_name, expectsql, argument, expect_db_rows):
"""
:param case_name: 用例名稱
:param expectsql: 預期sql語句
:param argument: 接口參數
:param expect_db_rows: 預期行數
:return:
"""
conn = self.conn_db() # 連接數據庫
try:
cursor = conn.cursor() # 創建游標
cursor.execute(expectsql) # 執行sql語句
dbactual = cursor.fetchone()[0] # 取第一行第一列數據
if dbactual == expect_db_rows:
log.log().info(case_name + '==落庫檢查==通過')
return True
else:
log.log().warning(case_name + '==落庫檢查==失敗==預期行數:' + str(expect_db_rows) + ',實際行數:' + str(dbactual))
return False
except Exception as e:
log.log().error('落庫檢查出錯' + e)
if __name__=='__main__':
DB().init_db()
DB().check_db('測試','select count(*) from user',{'name':123},3)
entry.py 文件代碼
import configparser
from project_M1.common import log
class Entry:
def __init__(self):
try:
conf = configparser.ConfigParser()
conf.read('../conf/entry.ini', encoding='utf-8')
self.which_server = conf.get('entry', 'which_server')
self.which_db = conf.get('entry', 'which_db')
except Exception as e:
log.log().error('讀取接口配置文件entry.ini失敗' + e)
def get_which_server(self):
# 根據目錄不同log.log()表示log文件中的函數
log.log().info('本次測試的接口服務器是:' + self.which_server)
return self.which_server
def get_which_db(self):
log.log().info('本次測試的數據庫服務器是:' + self.which_db)
return self.which_db
if __name__ == '__main__':
server = Entry().get_which_server()
db = Entry().get_which_db()
print('接口服務器是:' + server + ",數據庫服務器是:" + db)
log.py 文件代碼
import logging,time
def log():
"""
日志函數
:return:
"""
# 創建日志對象
logger = logging.getLogger()
# 禁止日志重復輸出 ,每個日志輸出一次
if not logger.handlers:
# 指定日志輸出級別
logger.setLevel(logging.INFO) # 高於INFO的信息都輸出到日志
# 指定日志中的輸出格式
# asctime 當前日志時間
# 當前日期時間 - 類型 - 文件名[行號] - 消息
formatter = logging.Formatter('%(asctime)s-%(levelname)s - %(filename)s[%(lineno)d]-%(message)s')
# 創建日志文件
now = time.strftime('%Y%m%d_%H%M%S')
log_file_name = '../log/' + now + '.log'
# log_file_name = '../log/' + time.strftime('%Y%m%d') + '.log'
# 打開日志文件
logfile = open(log_file_name, 'wb') # w:寫 ,b:字節
# 創建處理器
console = logging.StreamHandler() # 流處理器 輸出控制台
filehand = logging.FileHandler(log_file_name, encoding='utf-8') # 文件處理器 輸出文件
# 指定處理器的日志輸出格式
console.setFormatter(formatter)
filehand.setFormatter(formatter)
# 增加處理器到日志對象
logger.addHandler(console)
logger.addHandler(filehand)
# 關閉處理器
console.close()
filehand.close()
# 返回日志對象
return logger
# 調試
if __name__=='__main__':# 在這個文件(log.py)中執行時,下面的代碼才執行
log().info('成功時的日志')
log().warning('測試失敗時的日志')
log().error('出錯時的日志')
postdata.py文件代碼
import requests
from project_M1.common import log
def post(address, argument, case_name, expect):
"""
:param address: 接口地址
:param argument: 請求參數
:param case_name: 用例名稱
:param expect: 預期結果
:return:
"""
try:
# 發送請求 判斷響應結果的正確性
res = requests.post(url=address, data=eval(argument))
if 'text/html' in res.headers['Content-Type']: # 表示響應內容的類型是字符串
actual = res.text
# 結果比對
if expect in actual:
log.log().info('比對登錄接口返回==' + case_name + '==通過')
return True
else:
log.log().warning('比對登錄接口返回==' + case_name + '==失敗==預期結果:' + expect + ',實際結果:' + actual)
return False
elif 'application/json' in res.headers['Content-Type']:
actual = res.json() # 實際結果(字典)
# 比對接口返回結果
if expect == actual:
log.log().info("比對注冊接口返回==" + case_name + '==通過')
return True
else:
log.log().warning('比對注冊接口返回==' + case_name + '==失敗==預期結果:' + str(expect) + ',實際結果:' + str(actual))
return False
else: # xml
pass # 暫時不寫代碼
except Exception as e:
log.log().error('發送數據,比對響應結果出錯')
if __name__ == '__main__':
post('http://192.168.139.137/exam/login/', {'username': 'admin', 'password': '123456'}, '成功登錄測試', '登錄成功')
server.py文件代碼
from project_M1.common import entry, log
import configparser # python自帶的模塊 不需要from
class ServerConf: # 獲取接口服務器的地址
def __init__(self):
try:
# 獲得要測試的接口服務器的名字
which_server = entry.Entry().get_which_server()
conf = configparser.ConfigParser()
# conf對象中read方法,讀取server.conf文件,字符編碼設置為utf-8
conf.read('../conf/server.conf', encoding='utf-8')
# 根據exam節點獲取鍵名IP所對應的值,賦值給ip變量
ip = conf.get(which_server, 'IP')
port = conf.get(which_server, 'port')
self.host = 'http://' + ip + ':' + port
except Exception as e: # Exception是一個關鍵字,表示所有的異常,e是別名
log.log().error('接口服務器地址[server.conf]獲取失敗' + e)
def get_host(self):
log.log().info('接口服務器地址==' + self.host)
return self.host
if __name__ == '__main__':
print(ServerConf().get_host())
db.conf 文件內容
[exam] ;exam項目 上課的案例
host=192.168.139.137
port=3306
user=root
password=123456
db=exam
[sign] ;發布會項目 上課的練習
host=192.168.139.137
port=3306
user=root
password=123456
db=guest
server.conf文件內容
[exam] ;書寫內容公司會有相應文檔 exam項目:包括登錄和注冊接口
IP=192.168.139.137
port=80
[sign];發布會項目:添加發布會、查詢發布會
IP=192.168.139.137
port=8000
entry.ini 文件內容
[entry];入口:用於指定測試使用的服務器
which_server =exam
which_db =exam
login.xlsx文件內容
case_name |
data |
expect |
測試登陸成功 |
{'username':'test01','password':'123456'} |
登錄驗證成功 |
測試用戶名為空 |
{'username':'','password':'123456'} |
用戶名或密碼為空 |
測試密碼為空 |
{'username':'test01','password':''} |
用戶名或密碼為空 |
測試用戶名和密碼為空 |
{'username':'','password':''} |
用戶名或密碼為空 |
測試用戶名錯誤 |
{'username':'test001','password':'123456'} |
用戶名或密碼錯誤 |
測試密碼錯誤 |
{'username':'test01','password':'123'} |
用戶名或密碼錯誤 |
測試用戶密碼都錯誤 |
{'username':'test001','password':'1236'} |
用戶名或密碼錯誤 |
signup.xlsx文件內容
case_name |
data |
expect |
expect_sql |
expect_db_rows |
軟件注冊成功 |
{'username':'test02','password':'123456','confirm':'123456','name':'測試02'} |
{'Status': 1000, 'Result': 'Success', 'Message': '注冊成功'} |
select count(*) from user where username='test02' |
1 |
測試用戶名被占用 |
{'username':'test03','password':'123456','confirm':'123456','name':'測試03'} |
{'Status': 1003, 'Result': 'Username test03 is taken', 'Message': '用戶名已被占用'} |
select count(*) from user where username='test03' |
1 |
測試兩個密碼不一致 |
{'username':'test04','password':'123456','confirm':'1234','name':'測試04'} |
{'Status': 1002, 'Result': 'Password Not Compare', 'Message': '兩次輸入密碼的不一致'} |
select count(*) from user where username='test04' |
0 |
測試用戶名為空 |
{'username':'','password':'123456','confirm':'123456','name':'測試012'} |
{'Status': 1001, 'Result': 'Input Incomplete', 'Message': '輸入信息不完整'} |
select count(*) from user where username='' |
0 |
測試密碼為空 |
{'username':'test05','password':'','confirm':'123456','name':'測試05'} |
{'Status': 1001, 'Result': 'Input Incomplete', 'Message': '輸入信息不完整'} |
select count(*) from user where username='test05' |
0 |
測試確認密碼為空 |
{'username':'test06','password':'123456','confirm':'','name':'測試06'} |
{'Status': 1001, 'Result': 'Input Incomplete', 'Message': '輸入信息不完整'} |
select count(*) from user where username='test06' |
0 |
測試用戶名密碼確認密碼均為空 |
{'username':'','password':'','confirm':'','name':''} |
{'Status': 1001, 'Result': 'Input Incomplete', 'Message': '輸入信息不完整'} |
select count(*) from user where username='' |
0 |
login.sql文件內容
--登陸接口:test01
delete from user where username = 'test01';
insert into user(id,username,password) values(2,'test01','123456')
signup.sql文件內容
-- 注冊接口:test02、test03
delete from user where username = 'test02'
delete from user where username = 'test03'
insert into user(id,username,password) values(3,'test03','123456')
login.py 文件代碼
from project_M1.common import db, log, serverconf, casedata, postdata
import unittest
from ddt import ddt, data, unpack
# 讀測試用例數據
cases = casedata.read_cases('login.xlsx') # 用例:用例名0、參數數據1、預期結果2
@ddt
class Login(unittest.TestCase): # unittest.TestCase不可以省略
@classmethod
def setUpClass(self) -> None: # Login類中所有測試用例之前先做下面代碼
# 數據庫初始化
db.DB().init_db(['login.sql'])
# 讀接口服務器地址
host = serverconf.ServerConf().get_host()
self.address = host + '/exam/login/'
@data(*cases) # @data參數中不能使用self.、cls.
# cases是二維列表,有多行多列,@data(*cases)意思是拆出多行用例,
# 一行用例做一次循環,循環一次執行一次所修飾的test方法,
# 假設cases中有5個子列表,可以理解為5行,就會循環執行test_login五次
@unpack # 對cases中的每一行中的列進行拆分
def test_login(self, case_name, argument, expect):
# self 不能省略 定義一個測試用例測試登錄接口測試用例
"""
登錄接口
:return:
"""
# 發送請求
result = postdata.post(self.address, argument, case_name, expect)
self.assertTrue(result)
if __name__ == '__main__': # 只有自己的文件才執行下面的代碼,別人導入自己的文件后不執行
unittest.main() # 執行所有test開頭的測試用例(類中的方法)
signup.py 文件代碼
from project_M1.common import db, log, serverconf, casedata, postdata
import unittest
from ddt import ddt, data, unpack
@ddt
class Signup(unittest.TestCase): # 測試用例類 測試類
# 讀注冊接口測試用例:signup.xlsx
cases = casedata.read_cases('signup.xlsx')
@classmethod
def setUpClass(self) -> None:
# 數據庫初始化(執行注冊接口的初始化語句)
db.DB().init_db(['signup.sql'])
# 接口地址
self.host = serverconf.ServerConf().get_host()
self.address = self.host + '/exam/signup/'
@data(*cases) # 拆出行
@unpack # 拆出列
def test_signup(self, case_name, argument, expect, expectsql, expect_db_rows): # 必須test開頭
"""
注冊接口
:return:
"""
# 發送請求
result = postdata.post(self.address, argument, case_name, eval(expect))
self.assertTrue(result)
# 落庫檢查
dbresult = db.DB().check_db(case_name, expectsql, argument, expect_db_rows)
self.assertTrue(dbresult)
if __name__ == '__main__':#只有自己的文件才執行下面的代碼,別人導入自己的文件后不執行
unittest.main()#執行所有test開頭的測試用例(類中的方法)
run.py 文件代碼
from project_M1.testcase import login, signup
import unittest, time
from project_M1.runtest import HTMLTestRunner
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.utils import formataddr
now = time.strftime('%Y%m%d_%H%M%S')
report_file = '../report/' + now + '.html'
report = open(report_file, 'wb')
runner = HTMLTestRunner.HTMLTestRunner(stream=report, title='exam接口自動化測試報告', description='測試環境描述')
suite = unittest.defaultTestLoader.discover('../testcase/', '*.py')
runner.run(suite)
report.close()
smtp = smtplib.SMTP('smtp.qq.com', 25)
smtp.login('779146330@qq.com', 'cjzzifekloejbecg')
sender = '暄總<779146330@qq.com'
receivers = '小暄總<sunbx@tedu.cn,testpm<779146330@qq.com'
mailbody = '''
暄總:<p>
你好,第13輪接口自動化測試已經完成,測試報告參見附件,謝謝!
'''
mail = MIMEMultipart()
body = MIMEText(mailbody, 'plain', 'utf-8')
mail.attach(body)
mail['From'] = formataddr(sender.split('<'))
to = [r.split('<') for r in receivers.split(',')]
tos = ''
for i in to:
tos = tos + formataddr(i) + ','
mail['To'] = tos
mail['Subject'] = '某項目自動化測試第幾輪測試報告'
attach1 = MIMEText(open(report_file, 'rb').read(), 'base64', 'utf-8')
attach1['Content-Type'] = 'application/octet-stream'
attach1['Content-Disposition'] = 'attachment; filename="test.html"'
mail.attach(attach1)
logfile = '../log/' + now + '.log'
attach2 = MIMEText(open(logfile, 'rb').read(), 'base64', 'utf-8')
attach2['Content-Type'] = 'application/octet-stream'
attach2['Content-Disposition'] = "attachment; filename='log.txt'"
mail.attach(attach2)
smtp.sendmail(sender, receivers, mail.as_string())
smtp.quit()
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,'&');
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 = 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.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)