公司需要设计一个稳定性测试,就是一直持续的跑不同的用例,直到人为停止,用例基本完成,基本框架思路就是随机选择一个testcase,跑完后输出结果。但存在一个问题,现在的unittest或nose测试报告都是测完所有case后再输出html报告,再次运行,又生成新的,没法再原来的报告中再填加结果。
就这样问题,基本解决思路就是直接写个生成html报告的模块,然后再次每次结果加入。正好最近在看tempest的东西,里面有个这样的模块,那就不重复造轮子了,直接拿过来用吧。
先看看生成html_log.py文件
import shutil
try:
import xml.etree.cElementTree as ET
except Exception:
import xml.etree.ElementTree as ET
TYPE_OK = 0
TYPE_ERROR = -1
TYPE_FAIL = -2
TYPE_SKIP = -3
class InfoParser(object):
def __init__(self, file_path, info):
self.file_path = file_path
self.info = info
def get_state(self):
if self.info.find('... ok') != -1:
return TYPE_OK
elif self.info.find('... ERROR') != -1:
return TYPE_ERROR
elif self.info.find('... FAIL') != -1:
return TYPE_FAIL
elif self.info.find('... SKIP') != -1:
return TYPE_SKIP
def get_detail(self):
if self.get_state() == 0:
return ''
else:
offset = self.info.find('Traceback')
if offset != -1:
temp = self.info[offset:]
return temp
else:
return ''
def get_class_name(self):
temp = self.get_testcase_name()
offset = temp.rfind('.')
if offset != -1:
temp = temp[0: offset]
return temp
else:
return ''
def get_testcase_name(self):
offset = self.info.find(' ...')
if offset != -1:
temp = self.info[0: offset]
return temp
return ''
def create_new_overview_node(self, root, state, class_name):
new_node = ET.Element('tr')
root.insert(len(root.getchildren()) - 1, new_node)
# testcase name
sub_node = ET.SubElement(new_node, 'td')
sub_node.text = class_name
for x in range(5):
sub_node = ET.SubElement(new_node, 'td')
sub_node.text = '0'
return new_node
def add_overview_node_detail(self, item, state):
item[5].text = str(int(item[5].text) + 1)
if state == TYPE_FAIL:
item[1].text = str(int(item[1].text) + 1)
item[1].set('class', 'failed')
elif state == TYPE_ERROR:
item[2].text = str(int(item[2].text) + 1)
item[2].set('class', 'failed')
elif state == TYPE_SKIP:
item[3].text = str(int(item[3].text) + 1)
elif state == TYPE_OK:
item[4].text = str(int(item[4].text) + 1)
def add_to_overview(self, root, state, class_name):
iter = root.getchildren()
for item in iter:
if item[0].text == class_name:
self.add_overview_node_detail(item, state)
# handle total
total_item = root[len(root.getchildren()) - 1]
self.add_overview_node_detail(total_item, state)
return
new_item = self.create_new_overview_node(root, state, class_name)
self.add_overview_node_detail(new_item, state)
# handle total
total_item = root[len(root.getchildren()) - 1]
self.add_overview_node_detail(total_item, state)
def add_to_failure(self, root):
new_item = ET.SubElement(root, 'section')
# name
sub_item = ET.SubElement(new_item, 'h3')
sub_item.text = self.get_testcase_name()
# details
sub_item = ET.SubElement(new_item, 'div')
sub_item.set('class', 'test-details')
detail_sub_item = ET.SubElement(sub_item, 'h4')
detail_sub_item.text = 'Detail'
detail_sub_item = ET.SubElement(sub_item, 'pre')
detail_sub_item.text = self.get_detail()
def add_to_all_tests(self, root, state):
new_item = ET.SubElement(root, 'li')
sub_item = ET.SubElement(new_item, 'a')
sub_item.text = self.get_testcase_name()
if state == TYPE_FAIL or state == TYPE_ERROR:
sub_item.set('class', 'failed')
else:
sub_item.set('class', 'success')
def write_log(self):
ret = self.get_state()
tree = ET.parse(self.file_path)
root = tree.getroot()
# add overview
self.add_to_overview(
root.find('body').find('overview').find('section').find('table'),
ret, self.get_class_name())
# add fail view
if ret == TYPE_FAIL or ret == TYPE_ERROR:
self.add_to_failure(
root.find('body').find('failure_details').
find('section').find('div'))
# add all tests
self.add_to_all_tests(
root.find('body').find('all_tests').find('section').find('ul'),
ret)
tree.write(self.file_path)
def create_log_from_file(path, souce):
shutil.copyfile(souce, path)
def add_log(path, info):
info_parser = InfoParser(path, info)
info_parser.write_log()
def format_testrunner_info(test, info, state):
result = ''
# handle name
name = str(test)
offset_b = name.find('(')
offset_e = name.find(')')
testcase_name = 'no name'
if offset_b != -1 and offset_e != -1:
testcase_name = name[offset_b + 1: offset_e]
offset_e = name.find(' (')
if offset_e != -1:
testcase_name += '.'
testcase_name += name[0: offset_e]
result = testcase_name
if state == TYPE_OK:
result += ' ... ok\n\n'
elif state == TYPE_ERROR:
result += ' ... ERROR\n\n'
elif state == TYPE_FAIL:
result += ' ... FAIL\n\n'
elif state == TYPE_SKIP:
result += ' ... SKIP\n\n'
result += info
return result
def add_testrunner_log(path, result, test_name):
# success
if result.wasSuccessful():
info = format_testrunner_info(test_name, 'Ran 1 test', TYPE_OK)
#info = format_testrunner_info(name,'Ran 1 test',TYPE_OK)
add_log(path, info)
# fail
for test, err in result.failures:
info = format_testrunner_info(test, err, TYPE_FAIL)
add_log(path, info)
# error
for name, err in result.errors:
info = format_testrunner_info(name, err, TYPE_ERROR)
add_log(path, info)
# skip
for test, reason in result.skipped:
info = format_testrunner_info(test, reason, TYPE_SKIP)
add_log(path, info)
使用非常简单,使用对应的空报告模板,使生成的结果调用add_testrunner_log写入空模板中,最后就可以持续生成报告了
空模板见下面的html代码,copy下来后存成html文件即可使用
<!DOCTYPE html> <html> <head> <title>Unit Test Report</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <style> body { font-family: Calibri, "Trebuchet MS", sans-serif; } * { word-break: break-all; } table, td, th, .dataid { border: 1px solid #aaa; border-collapse: collapse; background: #fff; } section { background: rgba(0, 0, 0, 0.05); margin: 2ex; padding: 1ex; border: 1px solid #999; border-radius: 5px; } h1 { font-size: 130%; } h2 { font-size: 120%; } h3 { font-size: 100%; } h4 { font-size: 85%; } h1, h2, h3, h4, a[href] { cursor: pointer; color: #0074d9; text-decoration: none; } h3 strong, a.failed { color: #ff4136; } .failed { color: #ff4136; } a.success { color: #3d9970; } pre { font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', 'Monaco', 'Courier New', monospace; } .test-details, .traceback { display: none; } section:target .test-details { display: block; } </style> </head> <body> <overview> <h1>Overview</h1> <section> <table> <tr> <th>Class</th> <th class="failed">Fail</th> <th class="failed">Error</th> <th>Skip</th> <th>Success</th> <th>Total</th> </tr> <tr> <td><strong>Total</strong></td> <td>0</td> <td>0</td> <td>0</td> <td>0</td> <td>0</td> </tr> </table> </section> </overview> <failure_details> <h1>Failure details</h1> <section> <h2>Failure details</h2> <div> </div> </section> </failure_details> <all_tests> <h1>All tests</h1> <section> <h2>all tests</h2> <ul> </ul> </section> </all_tests> </body> <script> Array.prototype.forEach.call(document.querySelectorAll('h1, h2, h3, h4'), function(el) { el.addEventListener('click', function() { el.nextElementSibling.style.display = document.defaultView.getComputedStyle(el.nextElementSibling).display == 'none' ? 'block' : 'none'; }) }) </script>
使用unittest写个示范代码如下:
import unittest import time import sys import html_log import os import re import random class test(unittest.TestCase): def setUp(self): pass def test_0001(self): assert 1==1 def test_0002(self): assert 2==2 if __name__=='__main__': suite=unittest.TestLoader().loadTestsFromTestCase(test) testcases=list() listcasedir='.' testunit=unittest.TestSuite() #选择case for test_case in suite: print test_case f=re.match("(test_.*) \(__main__.test\)",str(test_case)) tt=f.group(1) testcases.append(test_case) test = random.choice(testcases) mySuite = unittest.TestSuite() mySuite.addTest(test) result = unittest.TextTestRunner().run(mySuite) #这里xx.html就是对应的空模板
if not os.path.exists('test_aa.html'): html_log.create_log_from_file('test_aa.html', 'xx.html')
#将结果持续加入到对应的html报告中 print test html_log.add_testrunner_log('test_aa.html', result, test)
结果如下,测试一直在进行,结果就一直会持续输入