项目目录结构规划:
bbs:用于存放BBS项目的测试用例、测试报告和测试数据等。
driver:用于存放浏览器驱动。如selenium-server-standalone-2.47.0.jar、chromedriver.exe、IEDriverServer.exe等。在执行测试前根据执行场景将浏览器驱动复制到系统环境变量path目录下。
package:用于存放自动化所用到的扩展包。例如,HTMLTestRunner.py属于一个单独模块,并且对其做了修改,所以,在执行测试前需要将它复制到Python的Lib目录下。
run_bbs_test.py:项目主程序。用来运行社区(BBS)自动化用例。
startup.bat:用于启动Selenium Server,默认启动driver目录下的selenium-server-standalone-2.47.0.jar。
自动化测试项目说明文档.docx:介绍当前项目的架构、配置和使用说明。
data:该目录用来存放测试相关的数据。
report:用于存放HTML测试报告。其下面创建了image目录用于存放测试过程中的截图。
test_case:测试用例目录,用于存放测试用例及相关模块。
models:该目录下存放了一些公共的配置函数及公共类。
page_obj:该目录用于存放测试用例的页面对象(Page Object)。根据自定义规则,以“*Page.py”命名的文件为封装的页面对象文件。
*stapy:测试用例文件。根据测试文件匹配规则,以“*_sta.py”命名的文件将被当作自动化测试用例执行。
1、定义浏览器驱动函数browser(),该函数可以进行配置,根据我们的需求,配置测试用例在不同的主机及浏览器下运行。
from selenium.webdriver import Remote
from selenium import webdriver
#启动浏览器驱动:
def browser():
driver = webdriver.Chrome()
host = "127.0.0.1:4444"
dc = {"browserName":"chrome"} #指定浏览器(chrome','firefox',)
driver = Remote(command_executor="http://" + host + "/wd/hub",desired_capabilities=dc)
return driver
if __name__ == '__main__':
dr = browser()
dr.get("http://www.baidu.com")
dr.quit()
2、自定义测试框架类myunit.py,定义MyTest()类用于继承unitest.TestCase类,因为笔者创建的所有测试类中setUp)与tearDown)方法所做的事情相同,所以,将它们抽象为MyTest()类,好处就是在编写测试用例时不再考虑这两个方法的实现。
from selenium import webdriver
from .driver import browser
import unittest
import os
class MyTest(unittest.TestCase):
def setUp(self):
self.driver = browser()
self.driver.implicitly_wait(10)
self.driver.maximize_window()
def tearDown(self):
self.driver.quit()
3、定义截图函数function.py,创建截图函数insert_img(),为了保持自动化项目的移植性,采用相对路径的方式将测试截图保存到.(reportimage目录中。
from selenium import webdriver
import os
#截图函数:
def insert_img(driver,file_name):
base_dir = os.path.dirname(os.path.dirname(__file__))
base_dir = str(base_dir)
base_dir = base_dir.replace("\","/")
base = base_dir.split("/test_case")[0]
file_path = base + "/report/image/" + file_name
driver.get_screenshot_as_file(file_path)
if __name__ == '__main__':
driver = webdriver.Chrome()
driver.get("https://www.baidu.com")
insert_img(driver,"baidu.jpg")
driver.quit()
4、创建基础Page基础类,创建页面基础类,通过__init__方法初始化参数:浏览器驱动、URL地址、超时时长等。定义基本方法:open()用于打开BBS地址;find element()和find elements()分别用来定位单个与多个元素;创建script)方法可以更简便地调用JavaScript代码。当然我们还可以对更多的WebDriver方法进行重定义。
5、创建BBS登录对象类loginPage.py,创建登录页面对象,对用户登录面上的用户名/密码输入框、登录按钮和提示信息等元素的定位进行封装。除此之外,还创建user_login()方法作为系统统一登录的入口。关于对操作步骤的封装既可以放在Page Object当中,也可以放在测试用例当中,这个主要根据具体需求来衡量。这里之所以存放在Page Object当中,主要考虑到还有其他用例会调用到该登录方法。为username和password入参设置了默认值是为了方便其他用例在调用user_login)时不用再传递登录用户信息,因为该系统大多用例的执行使用该账号即可,同时也方便了在账号失效时的修改。
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from .base import Page
from time import sleep
class login(Page):
"""
用户登录界面
"""
url = "/"
bbs_login_user_loc = (By.XPATH,"//div[@id='mzCust']/div/img")
bbs_login_button_loc = (By.ID,"mzLogin")
def bbs_login(self):
self.find_element(*self.bbs_login_user_loc).click()
sleep(1)
self.find_element(*self.bbs_login_button_loc).click()
login_username_loc = (By.ID,"account")
login_password_loc = (By.ID,"password")
login_button_loc = (By.ID,"login")
#登录用户名
def login_username(self,username):
self.find_element(*self.login_username_loc).send_keys(username)
#登录密码:
def login_password(self,password):
self.find_element(*self.login_password_loc).send_keys(password)
#登录按钮
def login_button(self):
self.find_element(*self.login_button_loc).click()
#定义统一登录入口:
def user_login(self,username = "username",password = "1111"):
"""获取用户名密码登录"""
self.open()
self.bbs_login()
self.login_username(username)
self.login_password(password)
self.login_button()
sleep(1)
user_error_hint_loc = (By.XPATH,"//span[@for = 'account']")
pawd_error_hint_loc = (By.XPATH,"//span[@for = 'password']")
user_login_success_loc = (By.ID,"mzCustName")
#用户名错误提示
def user_error_hint(self):
return self.find_element(*self.user_error_hint_loc).text
#密码错误提示
def pawd_error_hint(self):
return self.find_element(*self.pawd_error_hint_loc).text
#登录成功用户名
def user_login_success(self):
return self.find_element(*self.user_login_success_loc).text
6、首先创建loginTest()类,继承myunit.MyTest)类,关于MyTest)类的实现,请翻看前面的代码。这样就省去在了在每个测试类中实现一遍setUp()和tearDown0方法。
创建 user_login_verify)方法,并调用loginPage.py中定义的user_login)方法。为什么不直接调用呢?因为user_login)的入参已经设置了默认值,原因前面已经解释,这里需要重新将其入参的默认值设置为空即可。前三条测试用例很好理解,分别验证:
·用户名密码为空,点击登录;
·用户名正确,密码为空,点击登录;·用户名为空,密码正确,点击登录。
第四条用例验证错误的用户名和密码登录。在当前系统中如果反复使用固定且错误的用户名和密码,系统会弹出验证码输入框。为了避免这种情况的发生,就需要用户名进行随机变化,此处的做法用固定的前缀“zhangsan”,末尾字符从a-z中随机一个字符与前缀进行拼接。
第五条用例验证正确的用户名和密码登录,通过获取用户名作为断言信息。
在上面的测试用例中,每条测试用例结束时都调用function.py文件中的insertimg函数进行截图。当用例运行完成后,打开.…/report/image/目录将会看到用例执行的截图文件,如图11.3所示。
为了在测试用例运行过程中不影响做其他事,笔者选择调用远程主机或虚拟机来运行测试用例,那么这里就需要使用Selenium Grid(其包含在Selenium Server)来调用远程节创建../mztestprostartup.bat 文件,用于启动….mztestproldriver目录下的Selenium Server。
from time import sleep
import unittest,random,sys
sys.path.append("./models")
sys.path.append("./page_obj")
from models import myunit,function
from page_obj.loginPage import login
class loginTest(myunit.MyTest):
"""社区登录测试"""
#测试用户登录
def user_login_verify(self,username = "",password = ""):
login(self.driver).user_login(username,password)
def test_login1(self):
"""用户名、密码为空"""
self.user_login_verify()
po = login(self.driver)
self.assertEqual(po.user_error_hint(),"账号不能为空")
self.assertEqual(po.pawd_error_hint(),"密码不能为空")
function.insert_img(self.driver,"user_pawd_empty.jpg")
def test_login2(self):
"""用户名正确、密码为空登录"""
self.user_login_verify(username="pytest")
po = login(self.driver)
self.assertEqual(po.pawd_error_hint(),"密码不能为空")
function.insert_img(self.driver,"pawd_empty.jpg")
def test_login3(self):
"""用户名为空、密码为正确"""
self.user_login_verify(password="abc123456")
po = login(self.driver)
self.assertEqual(po.user_error_hint(),"账号不能为空")
function.insert_img(self.driver,"user_empty.jpg")
def test_login4(self):
"""用户名与密码不匹配"""
character = random.choice("zyxwvutsrqponmlkjihgfedcba")
username = "zhangsan" + character
self.user_login_verify(username = username,password="123456")
po = login(self.driver)
self.assertEqual(po.pawd_error_hint(),"账号与密码不匹配")
function.insert_img(self.driver,"user_pawd_error.jpg")
def test_login5(self):
"""用户名、密码正确"""
self.user_login_verify(username = zhangsan,password="123456")
sleep(2)
po = login(self.driver)
self.assertEqual(po.user_login_success(),"张三")
function.insert_img(self.driver,"user_pawd_ture.jpg")
if __name__ == '__main__':
unittest.main()
7、双击strtup.bat文件,启动Selenium Server创建主hub节点。在远程主机或虚拟机中同样需要启动Selenium Server创建node节点,创建方式参考本书第9.3节。
创建用例执行程序:….mztestpro/run_bbs_test.py
from HTMLTestRunner import HTMLTestRunner
from email.mime.text import MIMEText
from email.header import Header
import smtplib
import unittest
import time
import os
#定义发送邮件:
def send_mail(file_new):
f = open(file_new,"rb")
mail_body = f.read()
f.close()
msg = MIMEText(mail_body,"html","utf-8")
msg["Subject"] = Header("自动化测试报告","utf-8")
smtp = smtplib.SMTP()
smtp.connect("smtp.126.com")
smtp.login("username@126.com","123456")
smtp.sendmail("username@126.com","receive@126.com",msg.as_string())
smtp.quit()
print("email has send out !")
#查找测试报告目录、找到最新生成的测试报告文件
def new_report(testreport):
lists = os.listdir(testreport)
lists.sort(key=lambda fn:os.path.getmtime(testreport + "\" + fn))
file_new = os.path.join(testreport,lists[-1])
print(file_new)
return file_new
if __name__ == '__main__':
now = time.strftime("%Y-%m-%d %H_%M_%S")
filename = "./bbs/report/" + now + "result.html"
fp = open(filename,"wb")
runner = HTMLTestRunner(stream=fp,title="魅族社区自动化测试报告",description="环境:windows 7 浏览器:chrome")
discover = unittest.defaultTestLoader.discover("./bbs/test_case",pattern="*_sta.py")
runner.run(discover)
fp.close() #关闭测试报告
file_path = new_report("./bbs/report/") #查找新生成的报告
#发送测试报告:
send_mail(file_path)
8、执行过程中并没有做任何改动,集成了HTMLTestRunner生成HTML测试报告,以及
集成自动发邮件功能等。唯一需要注意的是,脚本中的路径建议使用相对路径,以便于项目被移动到任意目录下执行。
打开.modelsdriver.py文件,修改脚本运行的节点及浏览器。现在可以通过运行run_bbs_test.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(" ");
d.write("<a href='javascript:window.close()'>close</a> ");
d.write("</pre> ");
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;
font-family: "Lucida Console", "Courier New", Courier, monospace;
text-align: left;
font-size: 8pt;
500px;
}
}
/* -- report ------------------------------------------------------------------------ */
#show_detail_line {
margin-top: 3ex;
margin-bottom: 1ex;
}
#result_table {
80%;
border-collapse: collapse;
border: 1px solid #777;
}
#header_row {
font-weight: bold;
color: white;
}
#result_table td {
border: 1px solid #777;
padding: 2px;
}
#total_row { font-weight: bold; }
.passClass { }
.failClass { background-color: #c60; }
.errorClass { }
.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(' ')
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(' ')
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(' ')
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, ' Time 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(" ")[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 and mess up formating
# uo = unicode(o.encode('string_escape'))
uo = e
else:
uo = o
if isinstance(e,str):
# TODO: some problem with 'string_escape': it escape 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)