声明:本文着重讲框架,不讲解具体的python语法。阅读前最好先了解python语言、selenium和unitest的基础。代码里的内容在代码中标明里注释,没有在解释的部分说太多。
本文的测试框架基于python3+selenium+webdriver+unittest的,用于web网页的自动化(适用于PC+H5页面),主要解决以下几个问题:
1.定位器可配置(涉及PageElementLocator.ini,ParseConfigurationFile.py,VarConfig.py)
2.封装页面(涉及LoginPage.py)
3.数据分离(涉及ParseExcel.py,VarConfig.py)
4.业务逻辑封装(涉及LoginAction.py)
5.日志记录(涉及Logger.conf,Log.py)
6.生成测试报告(涉及HTMLTestRunner.py,RunTest.py)
框架的目录结构如下(pycharm里工程目录结构):
接下来我们通过“登陆”这个功能来串一下整个框架,登陆页如图所示:
1.定位器配置文件:PageElementLocator.ini文件
使用selenium能够定位到的方式,定位这个页面所有的元素(这里我采用的是xpath,不了解xpath的移步我的另外一篇文章《精简xpath定位总结》),并保存在config包中的PageElementLocator.ini文件里。这个登陆页有三个元素:用户名输入框、密码输入框和登陆按钮,如下代码所示:
[login]
loginPage.username=xpath>//*[@id="username"]
loginPage.password=xpath>//*[@id="password"]
loginPage.loginButton=xpath>//*[@id="loginBtn"]
以上的式子的表达式中,=号前面是该定位表达式对应某个页面类的某个定位方法;>号前是定位方式(xpath),后面是具体的定位表达式。页面类见下一步
2.封装页面类:LoginPage.py文件
这个文件在pageObjects包中,它通过解析在1步骤中的定位器文件,可以获得元素的定位方式和定位表达式,从而封装了几个方法并可以返回对应的element,以提供给后续的元素操作做准备,主要分为解析定位器文件和定位元素两个部分,代码如下:
from util.ObjectMap import *
from util.ParseConfigurationFile import ParseConfigFile
from util.Log import *
class LoginPage:
def __init__(self,driver):
self.driver = driver
#第一部分:解析定位器文件
self.parseCF=ParseConfigFile()
self.loginOptions=self.parseCF.getItemsSection('login')
def userNameObj(self):
try:
#根据解析定位文件获得定位方式和定位表达式
locateType,locateExpression = self.loginOptions['loginPage.username'.lower()].split('>')
print(locateType,locateExpression)
#第二部分:根据获得的定位方式和定位表达式定位元素
element = getElement(self.driver,locateType,locateExpression)
except Exception as e:
logger.error(e)
else:
logger.info("找到元素"+locateExpression)
return element
def passwordObj(self):
try:
locateType,locateExpression = self.loginOptions['loginPage.password'.lower()].split('>')
element = getElement(self.driver,locateType,locateExpression)
except Exception as e:
logger.error(e)
else:
logger.info("找到元素" + locateExpression)
return element
def loginButton(self):
try:
locateType,locateExpression = self.loginOptions['loginPage.loginButton'.lower()].split('>')
element = getElement(self.driver,locateType,locateExpression)
except Exception as e:
logger.error(e)
else:
logger.info("找到元素" + locateExpression)
return element
if __name__=="__main__":
from selenium import webdriver
import time
from util.SimulateLogin import simulator_login
browser = webdriver.Chrome()
browser.get("https://plogin.m.jd.com/user/login.action")
login=LoginPage(browser)
time.sleep(2)
login.userNameObj().send_keys("13180314708")
login.passwordObj().send_keys("liujinhong1995")
login.loginButton().click()
time.sleep(3)
simulator_login(browser)
browser.quit()
第1部分解析定位文件。我们用到了几个其他的文件,第一个是util包中的ParseConfigurationFile.py:
from configparser import ConfigParser
from config.VarConfig import pageElementLocatorPath
class ParseConfigFile:
def __init__(self):
self.cf=ConfigParser()
self.cf.read(pageElementLocatorPath)
#items方法获取的结果里把字符都转成小写了
def getItemsSection(self,sectionName):
optionsDict=self.cf.items(sectionName)
return dict(optionsDict)
def getOptionValue(self,sectionName,optionName):
value=self.cf.get(sectionName,optionName)
return dict(value)
if __name__=="__main__":
pc=ParseConfigFile()
print(pc.getItemsSection('login'))
print(pc.getOptionValue('login','loginPage.username'))
这个文件的使用了第三方包ConfigParser,该包可以把PageElementLocator.ini文件里的的定位表达式转换成字典,key是=号前面的部分,value是=后面的部分。sectionName为PageElementLocator.ini里中括号内的部分([login]),以区分不同页面的元素。ParseConfigurationFile.py里from config.VarConfig import pageElementLocatorPath,这句是获取到定位器文件的路径,config包中的模块VarConfig.py内容如下:
#coding=utf-8
import os
#获取当前文件所在目录的父目录的绝对路径
parentDirPath=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
#获取存放页面元素定位表达式文件的绝对路径
pageElementLocatorPath=parentDirPath+r"/config/PageElementLocator.ini"
#获取数据文件存放的绝对路径
dataFilePath=parentDirPath+r"/testData/登陆账号.xlsx"
#登陆账号.xlsx中每列对应的数字序号
acount_username=2
acount_password=3
acount_isExecute=4
acount_type=5
acount_comment=6
execute_testResult=7
execute_time=8
if __name__=="__main__":
print(pageElementLocatorPath)
print(dataFilePath)
第2部分定位元素。 这句代码 element = getElement(self.driver,locateType,locateExpression)
,getElement方法来源于util包中的ObjectMap模块,它的三个参数分别是:driver、定位方式和定位表达式。该方法封装了webdriver的WebDriverWait方法(这里不再详细展开,不了解的可以百度下),用于更方便的定位一个元素,ObjectMap.py的代码如下:
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
def getElement(driver,locateType,locateExpression):
try:
element=WebDriverWait(driver,30).until(lambda x:x.find_element(by=locateType,value=locateExpression))
return element
except Exception as e:
print(e)
def getElements(driver,locateType,locateExpression):
try:
elements = WebDriverWait(driver,30).until(lambda x:x.find_elements(by=locateType,value=locateExpression))
return elements
except Exception as e:
print(e)
if __name__=='__main__':
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("http://www.baidu.com")
#driver.find_element("id","kw").send_keys("selenium")
searchBox=getElement(driver,"id","kw")
print(searchBox.tag_name)
elements = getElements(driver,"tag name","a")
print(len(elements))
driver.quit()
定位方式和定位表达式来源1部分中的解析结果。
3.数据分离
这里的数据指的是测试脚本里需要用到的数据,比如要测试登陆,就要有正确的用户名和密码,错误的用户名和密码等等。除此之外,还要把测试脚本的执行结果作为数据保存起来。我们采用excel文件的方式来保存测试数据和执行结果,该文件存于testData包中登陆账号.xlsx,如图所示:
这里我们采用了第三方包openpyxl来做excel的操作,自己封装类一个ParseExcel类,使得调用更为方便,该类存于util包的ParseExcel.py文件中,实现了指定行号和列号取数据,指定列号取数据,向指定的单元格写入数据等方法,代码如下:
from openpyxl import Workbook
from openpyxl import load_workbook
from openpyxl.styles import colors
from openpyxl.styles import Font
import locale
import time
class ParseExcel(object):
def __init__(self, excel_file_path):
self.excel_file_path = excel_file_path
self.wb = load_workbook(excel_file_path)
self.ws = self.wb[self.wb.sheetnames[0]]
# print(self.ws.title)
def get_all_sheet_names(self):
return self.wb.sheetnames
def get_sheet_name_by_index(self, index):
return self.wb.sheetnames[index - 1]
def get_excel_file_path(self):
return self.excel_file_path
def create_sheet(self, sheet_name, position=None):
try:
if position:
self.wb.create_sheet(sheet_name, position)
else:
self.wb.create_sheet(sheet_name)
self.save()
return True
except Exception as e:
print(e)
return False
def get_sheet_by_name(self,sheet_name):
return self.wb.get_sheet_by_name(sheet_name)
def set_sheet_by_name(self, sheet_name):
if sheet_name not in self.wb.sheetnames:
print("%s sheet不存在,请重新设置!" % sheet_name)
return False
self.ws = self.wb[sheet_name]
return True
def set_sheet_by_index(self, index):
self.ws = self.wb[self.get_sheet_name_by_index(index)]
print("设定的sheet名称是:", self.ws.title)
def get_cell_value(self, row_no, col_no, sheet_name=None):
if sheet_name is not None: # 参数设置了新的sheet
result = self.set_sheet_by_name(sheet_name)
if result == False:
return None
return self.ws.cell(row_no, col_no).value
def get_row_values(self, row_no, sheet_name=None):
cell_values = []
if sheet_name is not None: # 参数设置了新的sheet
result = self.set_sheet_by_name(sheet_name)
if result == False:
return None
for cell in list(self.ws.rows)[row_no - 1]:
cell_values.append(cell.value)
return cell_values
def get_col_values(self, col_no, sheet_name=None):
cell_values = []
if sheet_name is not None: # 参数设置了新的sheet
result = self.set_sheet_by_name(sheet_name)
if result == False:
return None
for cell in list(self.ws.columns)[col_no - 1]:
cell_values.append(cell.value)
return cell_values
def get_some_values(self, min_row_no, min_col_no,max_row_no, max_col_no, sheet_name=None):
if sheet_name is not None: # 参数设置了新的sheet
result = self.set_sheet_by_name(sheet_name)
if result == False:
return None
values = []
for i in range(min_row_no, max_row_no + 1):
row_values = []
for j in range(min_col_no, max_col_no + 1):
row_values.append(self.ws.cell(row=i, column=j).value)
values.append(row_values)
return values
def save(self):
self.wb.save(self.excel_file_path)
def write_cell_value(self, row_no, col_no, value, style=None, sheet_name=None):
if sheet_name is not None: # 参数设置了新的sheet
result = self.set_sheet_by_name(sheet_name)
if result == False:
return False
if style is None:
style = colors.BLACK
elif style == "red":
style = colors.RED
elif style == "green":
style = colors.DARKGREEN
self.ws.cell(row=row_no, column=col_no).font = Font(color=style)
self.ws.cell(row=row_no, column=col_no, value=value)
self.save()
return True
def write_current_time(self, row_no, col_no, style=None, sheet_name=None):
if sheet_name is not None: # 参数设置了新的sheet
result = self.set_sheet_by_name(sheet_name)
if result == False:
return False
if style is None:
style = colors.BLACK
elif style == "red":
style = colors.RED
elif style == "greed":
style = colors.GREEN
locale.setlocale(locale.LC_ALL, 'en')
locale.setlocale(locale.LC_CTYPE, 'chinese')
self.ws.cell(row=row_no, column=col_no).font = Font(color=style)
self.ws.cell(row=row_no, column=col_no,
value=time.strftime("%Y年%m月%d日 %H时%M分%S秒"))
self.save()
return True
if __name__ == "__main__":
excel = ParseExcel(r"D:\study\光荣之路\正式课\第十九天\test.xlsx")
# print(excel.get_excel_file_path())
# print(excel.get_cell_value(1,1))
# print(excel.get_cell_value(3,3))
# excel.set_sheet_by_name("xxxx")
# excel.set_sheet_by_name("Sheet2")
# print(excel.get_cell_value(3,3))
# print(excel.get_cell_value(3,3,"xxx"))
# print(excel.get_cell_value(3,3,"Sheet2"))
# print(excel.get_row_values(1))
# print(excel.get_row_values(1,"Sheet2"))
# print(excel.get_col_values(1))
# print(excel.get_col_values(1,"Sheet2"))
# print(excel.get_some_values(1,1,5,5))
# print(excel.get_some_values(1,1,3,3,"Sheet2"))
# print(excel.write_cell_value(6,1,"光荣之路吴老师","red"))
# print(excel.write_current_time(6,1,"red"))
# print(excel.get_all_sheet_names())
# print(excel.get_sheet_name_by_index(1))
# excel.set_sheet_by_index(2)
print(excel.create_sheet("光荣之路"))
另外还在config包的VarConfig.py模块中做了数据文件的路径指定和文件中列号和数据字段的对应关系,上面有给出这个文件,这里我们用到了以下这几行:
#获取数据文件存放的绝对路径
dataFilePath=parentDirPath+r"/testData/登陆账号.xlsx"
#登陆账号.xlsx中每列对应的数字序号
acount_username=2
acount_password=3
acount_isExecute=4
acount_type=5
acount_comment=6
execute_testResult=7
execute_time=8
有了上面的基础,我们就可以在测试脚本中使用excel中的数据了,包testScripts中testLogin.py代码如下,具体代码不再做描述,代码中的注释已经说明:
#coding=utf-8
import time
import unittest
from selenium import webdriver
from appModules.LoginAction import LoginAction
from pageObjects.MinePage import MinePage
from util.ParseExcel import ParseExcel
from config.VarConfig import *
from util.Log import *
class TestLogin(unittest.TestCase):
#1.获取到文件存储路径 dataFilePath,并生成一个excelObj对象,用于操作excel
excelObj = ParseExcel(dataFilePath)
def setUp(self) -> None:
pass
def tearDown(self) -> None:
pass
def test_Login(self):
logger.info("开始执行登录脚本...")
#2.获取是否执行列,acount_isExecute来源于导入的模块from config.VarConfig import *
#acount_isExecute=4,isExecuteUser是一个存储来所有行是否执行的列表。是Y则执行,否则不执行
isExecuteUser=TestLogin.excelObj.get_col_values(acount_isExecute)
#遍历每行数据
for idx,i in enumerate(isExecuteUser[1:]):
start_time=time.time()
if i=='Y':
#获取指定单元格的数据
username=TestLogin.excelObj.get_cell_value(idx+2,acount_username)
password=TestLogin.excelObj.get_cell_value(idx+2,acount_password)
usertype=TestLogin.excelObj.get_cell_value(idx+2,acount_type)
logger.info("执行测试数据:%s,%s,%s"%(username,password,usertype))
try:
browser = webdriver.Chrome()
browser.get('http://test-jdread.jd.com/h5/m/p_my_details')
logger.info('启动浏览器,访问"我的"页面...')
minePage = MinePage(browser)
minePage.LoginEntryButton().click()
logger.info('点击"我的"页面的登录按钮...')
LoginAction.login(username, password, browser)
logger.info('登录操作执行...')
try:
minePage.ExitButtonObj() # 如果在"我的"页面找到退出按钮,则通过测试用例,如果没找到该按钮则测试用例未通过
logger.info('在"我的"页面找【退出】按钮')
except Exception as e:
self.assertTrue(1 == 2)
logger.debug('在"我的"页面找到【退出】按钮,失败,用例不通过')
#失败时:写入执行结果和执行时间
TestLogin.excelObj.write_cell_value(idx+2,execute_testResult,'fail','red')
TestLogin.excelObj.write_cell_value(idx + 2, execute_time,str(time.time()-start_time)+'ms', 'red')
else:
self.assertTrue(1 == 1)
logger.debug('在"我的"页面找到【退出】按钮,成功,用例通过')
#成功时:写入执行结果和执行时间
TestLogin.excelObj.write_cell_value(idx + 2, execute_testResult, 'success', 'green')
TestLogin.excelObj.write_cell_value(idx + 2, execute_time, str(round((time.time() - start_time)/1000,2)) + 's')
except Exception as e:
logger.error(e)
raise e
else:
continue
if __name__=="__main__":
# unittest.main()
#通过多个测试集合组成一个测试套
testsuit = unittest.TestSuite()
testsuit.addTest(TestLogin("test_Login"))
#运行测试套,verbosity=2说明输出每个测试用例运行的详细信息
unittest.TextTestRunner(verbosity=2).run(testsuit)
MinePage.py代码如下:
#coding=utf-8
from util.ObjectMap import *
from util.ParseConfigurationFile import ParseConfigFile
from util.Log import *
class MinePage:
def __init__(self,driver):
self.driver = driver
self.parseCF = ParseConfigFile()
self.mineOptions = self.parseCF.getItemsSection('mine')
#登陆状态
# minePage.exitButton = xpath > // *[text() = "退出登录"]
# minePage.loginEntryButton = xpath > // label[contains(text(), "点击登录")]
def ExitButtonObj(self):
try:
locateType,locateExpression = self.mineOptions['minePage.exitButton'.lower()].split('>')
element = getElement(self.driver,locateType,locateExpression)
except Exception as e:
logger.error(e)
else:
logger.info("找到元素" + locateExpression)
return element
#未登陆状态
def LoginEntryButton(self):
try:
locateType,locateExpression = self.mineOptions['minePage.loginEntryButton'.lower()].split('>')
element = getElement(self.driver,locateType,locateExpression)
except Exception as e:
logger.error(e)
else:
logger.info("找到元素" + locateExpression)
return element
def title(self):
try:
locateType,locateExpression = self.mineOptions['minePage.title'.lower()].split('>')
element = getElement(self.driver,locateType,locateExpression)
except Exception as e:
logger.error(e)
else:
logger.info("找到元素" + locateExpression)
return element
def exitDialogConfirm(self):
try:
locateType,locateExpression = self.mineOptions['minePage.exitDialogConfirm'.lower()].split('>')
element = getElement(self.driver,locateType,locateExpression)
except Exception as e:
logger.error(e)
else:
logger.info("找到元素" + locateExpression)
return element
if __name__=="__main__":
from selenium import webdriver
import time
browser = webdriver.Chrome()
#测试登陆入口按钮
browser.get('http://test-jdread.jd.com/h5/m/p_my_details')
minePage=MinePage(browser)
minePage.LoginEntryButton().click()
time.sleep(2)
browser.quit()
4.业务逻辑封装
对于某些业务逻辑非常复杂的脚本,需要我们把一些公共的模块抽象出来,以减少重复代码,提高代码的服用行,这些模块我们放到appMoudules包中,命名方式为xxxAction.py.这里我们通过登陆来举例,整个登陆需要三个元素,一个用户名输入框,一个密码输入框和一个登陆按钮,登陆逻辑就是把这三个元素的获取,点击按钮操作和登陆后跳转到指定页面,都封装到一个方法login里,如果什么地方用到登陆,可以直接调用这个方法。LoginAction.py具体代码如下:
#coding=utf-8
from pageObjects.LoginPage import LoginPage
from util.SimulateLogin import *
from util.Log import *
class LoginAction:
def __init__(self):
logger.info("login..")
@staticmethod
def login(username,password,browser,source_url=None):
try:
# browser.get("https://plogin.m.jd.com/user/login.action")
#使用了封装的页面类LoginPage
page = LoginPage(browser)
page.userNameObj().send_keys(username)
page.passwordObj().send_keys(password)
page.loginButton().click()
time.sleep(3)
while (1):
verify_code(browser)
try:
# 这个条件不同情况下调用需要修改
element = browser.find_element_by_xpath('//*[@id="captcha"]/div[1]')
except Exception as e:
logger.info("登录成功!")
if source_url:
browser.get(source_url)
return
except Exception as e:
logger.error(e)
raise e
if __name__=="__main__":
from selenium import webdriver
import time
browser=webdriver.Chrome()
browser.get("https://plogin.m.jd.com/user/login.action")
LoginAction.login('13180314708','liujinhong1995',browser)
browser.quit()
5.日志记录
这里采用了第三方包logging来记录日志,通过fileConfig的方法加载日志配置,在日志配置中设置不同的日志模版,模版里设置不同的日志等级:DEBUG、INFO、WARNING、ERROR、CRITICAL,设置不同的日志格式,设置日志的输出方式(文件、控制台等)。想详细了解的可以看这篇:https://www.jianshu.com/p/feb86c06c4f4
在util包中的Log.py对配置进行里加载,config包中的Logger.conf是配置文件,如下代码:
Log.py文件:
# -*- encoding:utf-8 -*-
import logging
import logging.config
from config.VarConfig import parentDirPath
logpath=parentDirPath+"/config/Logger.conf"
logging.config.fileConfig(logpath)
# create logger
#不同环境下只需要修改logger_name就可以切换日志的模板
logger_name = "example01"
logger = logging.getLogger(logger_name)
if __name__=="__main__":
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
Logger.conf文件
[loggers]
keys=root,example01,example02
#logger概述
[logger_root]
level=DEBUG
handlers=hand01,hand02,hand03,hand04
#一个日志输出的模板(测试环境)
[logger_example01]
handlers=hand01,hand02
qualname=example01
propagate=0
#一个日志输出的模板(线上环境,不需要输出debug和info)
[logger_example02]
handlers=hand03,hand04
qualname=example02
propagate=0
[handlers]
keys=hand01,hand02,hand03,hand04
[handler_hand01]
class=StreamHandler#把日志输出到控制台,日志级别大于等于INFO时输出
level=INFO
formatter=form01
args=(sys.stderr,)
[handler_hand02]
class=FileHandler#把日志输出到文件里,日志级别大于等于DEBUG时输出
level=DEBUG
formatter=form01
args=('../log/DataDrivenFrameWork_test.log', 'a')
[handler_hand03]
class=StreamHandler#把日志输出到控制台,日志级别大于等于WARNING时输出
level=WARNING
formatter=form01
args=(sys.stderr,)
[handler_hand04]
class=FileHandler#把日志输出到文件里,日志级别大于等于WARNING时输出
level=WARNING
formatter=form01
args=('../log/DataDrivenFrameWork_online.log', 'a')
[formatters]
keys=form01
[formatter_form01]
format=%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s
datefmt=%Y-%m-%d-%H:%M:%S
有了以上两个文件的基础,我们就能在测试脚本中需要的地方加log了。此处可以参考3中的testLogin.py代码。
在脚本执行前记录开始
logger.info("开始执行登录脚本...")
在执行完脚本后记录结果日志:
logger.debug('在"我的"页面找到【退出】按钮,失败,用例不通过')
报错的的时候记录日志:
logger.error(e)
你可以在任何你觉得需要记录的地方打log,一般会在脚本执行开始、结束和报错的时候记录日志。
6.生成测试报告
对于所有的测试脚本,我们应该有一个统一管理运行的文件,testScripts包中的RunTest.py,在这个文件中,把所有的脚本放到测试套件里面,运行完脚本后统一生成测试报告,这里我们使用里unittest的测试套。使用HTMLTestRunner.py生成测试报告(这个文件不需要仔细阅读,会用即可)。
RunTest.py文件:
#coding=utf-8
import unittest
import os
from util import HTMLTestRunner
if __name__=="__main__":
# 加载当前目录下所有有效的测试模块(以test开头的py文件),“.”表示当前目录
testSuite = unittest.TestLoader().discover('.')
filename = "../test.html" # 定义个报告存放路径,支持相对路径。
# 以二进制方式打开文件,准备写
fp = open(filename, 'wb')
# 使用HTMLTestRunner配置参数,输出报告路径、报告标题、描述,均可以配
runner = HTMLTestRunner.HTMLTestRunner(stream=fp,
title='测试报告', description='京东阅读M站自动化测试报告')
# 运行测试集合
runner.run(testSuite)
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 = ''
# 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
%(heading)s
%(report)s
%(ending)s