一、功能介绍
目前实现的功能有对接口进行测试(类里面进行用例编写)、excel读取用例、多个接口批量运行、生成报告、报告发送到邮箱。。。
整个流程就是:
1-导入功能包requests、unittest
2-创建一个测试类继承(unittest.TestCase)
3-写具体的测试内容setUp(),结束模块tearDown(),以及测试用例模块test_case() 【测试用例必须是test_ 开头】
4-组装testsuit(套件),批量运行多个测试类的用例
5-运行testsuit,生成测试报告
6-将测试报告发送到邮箱
每一个小功能网上都有具体的代码以及介绍,这个文章是日常使用中,将暂时用的功能汇总到一起,实现一个完整的demo。每个小功能就不具体的介绍了,不太清楚的可以去网上查下。
二、介绍unittest
因为框架是基于unittest写的,所以这里简单介绍下unittest
unittest官方文档:https://docs.python.org/2.7/library/unittest.html
2.1、unittest核心工作原理
介绍来源于以下链接:https://www.cnblogs.com/hackerain/p/3682019.html。详细内容可以移步该博客。
unittest中最核心的四个概念是:test case, test suite, test runner, test fixture。
下面我们分别来解释这四个概念的意思,先来看一张unittest的静态类图
- 一个TestCase的实例就是一个测试用例。什么是测试用例呢?就是一个完整的测试流程,包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown)。元测试(unit test)的本质也就在这里,一个测试用例是一个完整的测试单元,通过运行这个测试单元,可以对某一个问题进行验证。
- 而多个测试用例集合在一起,就是TestSuite,而且TestSuite也可以嵌套TestSuite。
- TestLoader是用来加载TestCase到TestSuite中的,其中有几个loadTestsFrom__()方法,就是从各个地方寻找TestCase,创建它们的实例,然后add到TestSuite中,再返回一个TestSuite实例。
- TextTestRunner是来执行测试用例的,其中的run(test)会执行TestSuite/TestCase中的run(result)方法。
- 测试的结果会保存到TextTestResult实例中,包括运行了多少测试用例,成功了多少,失败了多少等信息。
这样整个流程就清楚了,首先是要写好TestCase,然后由TestLoader加载TestCase到TestSuite,然后由TextTestRunner来运行TestSuite,运行的结果保存在TextTestResult中,整个过程集成在unittest.main模块中。
现在已经涉及到了test case, test suite, test runner这三个概念了,还有test fixture没有提到,那什么是test fixture呢??在TestCase的docstring中有这样一段话:
Test authors should subclass TestCase for their own tests. Construction and deconstruction of the test's environment ('fixture') can be implemented by overriding the 'setUp' and 'tearDown' methods respectively.
可见,对一个测试用例环境的搭建和销毁,是一个fixture,通过覆盖TestCase的setUp()和tearDown()方法来实现。这个有什么用呢?比如说在这个测试用例中需要访问数据库,那么可以在setUp()中建立数据库连接以及进行一些初始化,在tearDown()中清除在数据库中产生的数据,然后关闭连接。注意tearDown的过程很重要,要为以后的TestCase留下一个干净的环境。关于fixture,还有一个专门的库函数叫做fixtures,功能更加强大,以后会介绍到。
三、项目目录
四、依赖的环境
因为这个项目搭建的太久了,可能会漏掉一些包,所以直接把当前所有的包都导出来了,可以直接都下载,也可以选择哪些用得到的下载
airtest==1.1.11 allure-pytest==2.9.43 allure-python-commons==2.9.43 appdirs==1.4.4 asgiref==3.2.7 attrs==19.3.0 Automat==20.2.0 bcrypt==3.2.0 cached-property==1.5.2 certifi==2020.4.5.1 cffi==1.14.1 constantly==15.1.0 cryptography==3.0 cssselect==1.1.0 chardet==3.0.4 cycler==0.10.0 decorator==5.0.9 Deprecated==1.2.12 Django==3.0.5 django-cors-headers==3.3.0 facebook-wda==1.4.2 get==2019.4.13 hrpc==1.0.8 hyperlink==20.0.1 idna==2.9 imgkit==1.0.2 importlib-metadata==4.6.4 incremental==17.5.0 iniconfig==1.1.1 itemadapter==0.1.0 itemloaders==1.0.2 Jinja2==2.11.2 jmespath==0.10.0 kiwisolver==1.2.0 lxml==4.5.2 MarkupSafe==1.1.1 matplotlib==3.3.1 mss==4.0.3 numpy==1.19.1 opencv-contrib-python==4.5.2.52 packaging==21.0 paramiko==2.7.2 parsel==1.6.0 Pillow==7.2.0 pluggy==0.13.1 pocoui==1.0.82 post==2019.4.13 prettytable==0.7.2 Protego==0.1.16 public==2019.4.13 py==1.10.0 pyasn1==0.4.8 pyasn1-modules==0.2.8 pycparser==2.20 PyDispatcher==2.0.5 pyecharts==1.0.0 pyecharts-snapshot==0.2.0 pyee==7.0.3 pygame==2.0.0 PyHamcrest==2.0.2 PyMySQL==0.9.3 PyNaCl==1.4.0 pyOpenSSL==19.1.0 pyparsing==2.4.7 pyppeteer==0.2.2 pytest==6.2.4 pytest-html==3.1.1 pytest-metadata==1.11.0 python-dateutil==2.8.1 python-xlib==0.30 pytz==2019.3 pywinauto==0.6.3 PyYAML==5.4.1 query-string==2019.4.13 queuelib==1.5.0 request==2019.4.13 requests==2.23.0 retry==0.9.2 Scrapy==2.3.0 selenium==3.141.0 service-identity==18.1.0 simplejson==3.17.2 six==1.12.0 snapshot-phantomjs==0.0.3 snapshot-selenium==0.0.2 sqlparse==0.3.1 toml==0.10.2 tqdm==4.48.2 Twisted==20.3.0 typing-extensions==3.10.0.0 urllib3==1.25.9 w3lib==1.22.0 websocket-client==0.48.0 websockets==8.1 wrapt==1.12.1 xlrd==1.2.0 yagmail==0.11.224 zipp==3.5.0 zope.interface==5.
五、testcase编写
5.1、测试类里面编写用例
顾名思义就是将所有的测试用例,直接一条一条的放在测试类里面,每个用例以test_开头,每个用例是一个测试点。
直接上代码,拿登录接口做个demo
1 import json 2 import unittest 3 import requests 4 from lib.publicMethod.Log import logger 5 class Login(unittest.TestCase): 6 7 # 用例前准备 8 @classmethod 9 def setUpClass(cls): 10 11 cls.url = 'http://172.26.130.4:9898/alice/usr/loginSubmit' 12 cls.headers = {'Content-Type': 'application/json'} 13 14 # 调用接口,做接口的一些校验,封装公共方法,但这个不是用例 15 def resp_success(self, data, assert_code, assert_msg): 16 17 logger.info("入参: " + str(data)) 18 logger.info("url: " + str(self.url)) 19 20 response = requests.request("POST", self.url, data=data, headers=self.headers) 21 22 json_data = json.loads(response.text) 23 # 至此得到接口返回的数据 24 logger.info("接口返回的数据为:" + str(json_data)) 25 realStatus = json_data['status'] 26 logger.info("接口返回的状态码:" + str(realStatus)) 27 28 self.assertEqual(str(realStatus), str(assert_code), "返回码错误!!!") 29 # 错误case 比对错误信息 30 if str(realStatus) != str(1): 31 if json_data.get('message'): 32 realErrorMsg = json_data["message"] 33 else: 34 realErrorMsg = json_data["message"] 35 self.assertEqual(realErrorMsg, assert_msg, "错误信息不一致!!!") 36 37 # 用例1-成功的case 38 """成功""" 39 def test_success(self): 40 data = {"username":"zhangxue","password":"zhangxue1234"} 41 params = json.dumps(data) 42 self.resp_success(params, 1, 'OK') 43 44 # 用例2-失败的case 45 """密码错误""" 46 def test_passEmpty(self): 47 data = {"username":"zhangxue","password":"zhangxue123455"} 48 params = json.dumps(data) 49 self.resp_success(params, 0, '请检查用户名和密码是否正确!') 50 51 # 用例3-失败的case 52 """用户名为空""" 53 def test_passEmpty(self): 54 data = {"username": "", "password": "zhangxue123455"} 55 params = json.dumps(data) 56 self.resp_success(params, 0, '请检查用户名和密码是否正确!') 57 58 59 60 if __name__ == '__main__': 61 unittest.main()
按照上面的demo,格式就是如上,
1-导入相关包
import unittest、import requests
2-创建测试类,
class Login(unittest.TestCase)
3-写具体的用例,处理相关调用跟断言
4-main函数运行
unittest.main()
5.2、通过excel导入用例
5.2.1、先看下excel用例的大致格式
格式都可以根据自己的需求去调整
我的这个文件文件名称叫backend.xlsx,下面demo用的sheet是User_addConfig这个,具体的内容就是如图的内容。
一般都是将一个接口的用例放到一个sheet下,这个接口共有38个用例,有成功的,有失败的。根据接口的场景去设计用例内容
5.2.2、testcase文件
该文件要调用一个公共方法,就是lib---publicMethod下面的GenerateTestCases
该文件的getTest方法中,要对你的excel中的用例做兼容处理,处理每个用例的调用跟断言,这里需要根据具体的业务去实现
testcase文件
1 import json 2 import unittest 3 import requests 4 from lib.publicMethod.Log import logger 5 import sys 6 from lib.publicMethod.GenerateTestCases import __generateTestCases 7 import time 8 9 class User_addConfig(unittest.TestCase): 10 """用户添加配置信息""" 11 12 @classmethod 13 def setUpClass(cls): 14 cls.Url = backend_url + "/makeup/set_products" 15 bid = 'abc.zx' 16 cls.ts = int(time.time()) 17 cls.getmsg = GetSdkv5Msg() 18 cls.sign = cls.getmsg.get_sign(API_KEY_all, API_SECRET_all, cls.ts) 19 cls.ts = int(time.time()) 20 21 def getTest(self, txInfo): 22 23 t_num = txInfo["tc_num"] 24 t_name = txInfo["tc_name"] 25 t_params = txInfo["params"] 26 paramas = json.loads(t_params) 27 t_errcode = str(txInfo["code"]).split('.')[0] 28 t_sign = str(txInfo["sign"]) 29 t_error_msg = txInfo["error_msg"] 30 logger.info('测试的用例为:'+t_num+' '+t_name+' '+str(t_params)+' '+str(t_sign)) 31 32 if len(t_sign) > 0: 33 sign1 = json.loads(t_sign) 34 sign_api_key = str(sign1['sign_api_key']) 35 sign_api_secret = str(sign1['sign_api_secret']) 36 paramas['sign'] = self.getmsg.get_sign(sign_api_key, sign_api_secret, self.ts) 37 print(paramas['sign']) 38 39 data = json.dumps(paramas) 40 response = requests.request("POST", self.Url, data=data, headers=headers) 41 42 json_data = json.loads(response.text) 43 # 至此得到接口返回的数据 44 logger.info("接口返回的数据为:" + str(json_data)) 45 46 realStatus = response.status_code 47 logger.info("接口返回的状态码为:" +str(realStatus)) 48 49 if str(realStatus) == str(200): 50 self.assertEqual(str(realStatus), str(t_errcode), "返回码错误!!!") 51 elif str(realStatus) == str(400): 52 realErr_code = response.status_code 53 self.assertEqual(str(realErr_code), t_errcode, "错误码不一致!!!") 54 realErrorMsg = json_data["err_msg"] 55 self.assertEqual(realErrorMsg, t_error_msg, "错误信息不一致!!!") 56 57 @staticmethod 58 def getTestFunc(arg1): 59 def func(self): 60 self.getTest(arg1) 61 62 return func 63 64 65 # 类的实例、被测试的接口名称、测试数据文件名、excel数据表单名称 66 __generateTestCases(User_addConfig, "User_addConfig", "backend.xlsx", "User_addConfig") 67 68 if __name__ == '__main__': 69 unittest.main()
GenerateTestCases文件内容
1 import os 2 from lib.publicMethod import ReadExcelData 3 from config.config import datapath 4 5 # 写一个testcase 生成报告后,会有一个case的执行状态记录。这样我们写一个登录功能的自动化用例,只写一个case显然是不行的,测试用例要满足他的覆盖度,所以我们需要写多个用例。但是对于同样的功能,我们用例脚本体现出来的只有输入的参数值不一样,其它操作都是一样的。 6 # 7 # 这时候一个用例写一个test_case_login()的脚本,但是我们又想在报告中单独记录每一个case的执行状态,不得写多个重复的方法。 如: test_case_login_1() test_case_login_2() test_case_login_3() 这样执行完成后,使用unittest的进行生成测试报告,对每一个test_case都能记录执行状态。 8 # 但是代码太过冗余,内容太过笨重。 或许此时我们可以仅写一个test case并用内嵌循环来进行,但是会出现一个问题,就是其中一个出了错误,很难从测试结果里边看出来。 9 # 问题的关键在于是否有办法根据输入参数的不同组合产生出对应的test case。 比如我5组数据,就应该有5个test_case_login,上面我已经说过不适合直接写5个test_case_login,那么应该怎么做呢? 一种可能的思路是不利用unittest.TestCase这个类框中的test_成员函数的方法,而是自己写runTest这个成员函数,那样会有一些额外的工作,而且看起来不是那么“智能”, 10 # 如果目的是让框架自动调用test_case自然的思路就是 • 利用setattr来自动为已有的TestCase类添加成员函数 • 为了使这个方法凑效,需要用类的static method来生成decorate类的成员函数,并使该函数返回一个test函数对象出去 • 在某个地方注册这个添加test成员函数的调用(只需要在实际执行前就可以,可以放在模块中自动执行亦可以手动调用) 11 12 # 类的实例、被测试的接口名称、测试数据文件名、测试数据表单名称 13 def __generateTestCases(instanse, inerfaceName, tesDataName, sheetName): 14 file = os.path.join(datapath, tesDataName) 15 data_list = ReadExcelData.excel_to_list(file, sheetName) 16 for i in range(len(data_list)): 17 setattr(instanse, 'test_' + inerfaceName +'_%s' % (str(data_list[i]["test_num"])) +'_%s' % (str(data_list[i]["test_name"])), 18 instanse.getTestFunc(data_list[i])) 19 20 21 # if __name__ == '__main__': 22 # __generateTestCases()
GenerateTestCases文件以来一个lib.publicMethod import ReadExcelData
ReadExcelData文件内容
1 import xlrd 2 from config.config import datapath 3 4 ''' 5 读取excel文件,将excle文件里面对应的数据转换为python 列表对象,其中列表中每一个元素为一个字典 6 ''' 7 def excel_to_list(file, tag):#将excel表中数据转换成python对象 8 data_list = [] 9 book = xlrd.open_workbook(file)#打开excel文件 10 tag = book.sheet_by_name(tag)#选择读取sheet工作表 11 row_num = tag.nrows#获取行数 12 header = tag.row_values(0) 13 for i in range(1, row_num):#读取行 14 row_data = tag.row_values(i)#读取行中的每一列的值 15 d = dict(zip(header, row_data)) 16 data_list.append(d) 17 # print data_list 18 return data_list 19 20 # ''' 21 # 获取测试数据,判断传入的test_name 是否存在,存在则返回一个列表中对应的字典数据 22 # ''' 23 # def get_test_data(test_name, test_list): 24 # for test_dict in test_list: 25 # if test_name == test_dict['test_name']: 26 # return test_dict 27 28 if __name__ == '__main__': 29 # print(datapath+"############") 30 file = datapath+'ceshi.xlsx' 31 tagName = 'add' 32 test_list = excel_to_list(file, tagName)#获取excel文件one标签的所有数据,生成json 33 # [{'name': 'xiaoming', 'sex': '女', 'age': 18.0, 'id': 5345345.0},{'name': 'xiaohong', 'sex': '男', 'age': 50.0, 'id': 46456.0}] 34 print(test_list)
六、testsuite编写
6.1、根据不同的业务场景组装suite套件
testcase文件夹下所有.py的文件都装到suite里面
1 # 生成报告文件的参数 2 basedir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 3 # discover这个方法可以递归的去识别路径下所有符合pattern的文件,将这些文件加入套件 4 suite = unittest.defaultTestLoader.discover(basedir + '/test/testcase/', pattern='*.py')
常用的就是将testcase下面所有的都运行,如上。当然suite还有很多其他的内容,可以去网上查查看,下面就是一些其他的组装形式
1 # 生成报告文件的参数 2 basedir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 3 4 # 获取一个Testsuite对象 5 suite = unittest.TestSuite() 6 7 # discover这个方法可以递归的去识别路径下所有符合pattern的文件,将这些文件加入套件 8 suite = unittest.defaultTestLoader.discover(basedir+'/test/testcase/', pattern='*.py') 9 10 # discover这个方法可以递归的去识别路径下所有符合pattern的文件,将这些文件加入套件 11 suite = unittest.defaultTestLoader.discover(basedir+'/test/testcase/', pattern='*.py') 12 13 # 1、用TestSuite的addTests()方法,将测试用例组装成测试套件,不用TestLoader时,传入的是case里面的方法名。用TestLoader时传入类的类名 14 # addTest传入单个的Testcase方法,addTests传入Testcase方法数组 15 16 17 suite.addTest(TestMathFunc('test_add')) 18 tests = [TestMathFunc('test_add'),TestMathFunc('test_minus'),TestMathFunc('test_divide')] 19 suite.addTests(tests) 20 21 22 23 # 2、使用addTests+TestLoader传入测试用例,但是TestLoader无法对case排序 24 25 # 2.1 loadTestsFromName传入‘模块名.TestCase名’(文件名.里面的类名) 26 suite.addTests(unittest.TestLoader().loadTestsFromName('test_mathfunc.TestMathFunc')) 27 # 2.2 loadTestsFromNames就是传入列表即有多个testcase时,依次传入文件 28 suite.addTests(unittest.TestLoader().loadTestsFromNames(['test_mathfunc.TestMathFunc'])) 29 # 2.3 loadTestsFromTestCase传入TestCase名,(testcae中文件里面的类名) 30 suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc)) 31 suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TwoMathFun)) 32 suite.addTests(unittest.TestLoader().loadTestsFromTestCase(Add)) 33 34 # 运行suite,这样写是将结果输出到控制台 35 # verbosity参数可以控制输出的错误报告的详细程度,默认是1, 36 # 如果设为0,则不输出每一用例的执行结果,即没有上面的结果中的第1行; 37 # 如果设为2,则输出详细的执行结果 38 # runner = unittest.TextTestRunner(verbosity=2) 39 # runner.run(suite)
6.1、完整的testsuite文件
1 import unittest 2 import os 3 4 from lib.publicMethod.HTMLTestRunner_PY3 import HTMLTestRunner 5 import time 6 from lib.publicMethod import Send_email 7 8 def myrunner(): 9 # 生成报告文件的参数 10 basedir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 11 # discover这个方法可以递归的去识别路径下所有符合pattern的文件,将这些文件加入套件 12 suite = unittest.defaultTestLoader.discover(basedir + '/test/testcase/', pattern='*.py') 13 14 # 生成报告文件的参数 15 report_title = '接口自动化测试结果' 16 desc = '饼图统计测试执行情况' 17 report_file = basedir + '/report/testsuit.html' 18 with open(report_file, 'wb') as report: 19 runner = HTMLTestRunner(stream=report, title=report_title, description=desc) 20 runner.run(suite) 21 22 # 发送报告到邮箱 23 time.sleep(1) 24 Send_email.cr_zip('TestReport.zip', basedir + '/report/') 25 Send_email.send_mail_report("张雪测试!!!")
七、生成报告
7.1、这里需要用到一个公共的文件,报告模版
from lib.publicMethod.HTMLTestRunner_PY3 import HTMLTestRunner
就是报告的样式模板,网上都可以下载
大家可以百度网上去下载,也可以新建一个HTMLTestRunner_PY3文件,将下面代码考过去
1 # -*- coding: utf-8 -*- 2 """ 3 A TestRunner for use with the Python unit testing framework. It 4 generates a HTML report to show the result at a glance. 5 6 The simplest way to use this is to invoke its main method. E.g. 7 8 import unittest 9 import HTMLTestRunner 10 11 ... define your tests ... 12 13 if __name__ == '__main__': 14 HTMLTestRunner.main() 15 16 17 For more customization options, instantiates a HTMLTestRunner object. 18 HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g. 19 20 # output to a file 21 fp = file('my_report.html', 'wb') 22 runner = HTMLTestRunner.HTMLTestRunner( 23 stream=fp, 24 title='My unit test', 25 description='This demonstrates the report output by HTMLTestRunner.' 26 ) 27 28 # Use an external stylesheet. 29 # See the Template_mixin class for more customizable options 30 runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">' 31 32 # run the test 33 runner.run(my_test_suite) 34 35 36 ------------------------------------------------------------------------ 37 Copyright (c) 2004-2007, Wai Yip Tung 38 All rights reserved. 39 40 Redistribution and use in source and binary forms, with or without 41 modification, are permitted provided that the following conditions are 42 met: 43 44 * Redistributions of source code must retain the above copyright notice, 45 this list of conditions and the following disclaimer. 46 * Redistributions in binary form must reproduce the above copyright 47 notice, this list of conditions and the following disclaimer in the 48 documentation and/or other materials provided with the distribution. 49 * Neither the name Wai Yip Tung nor the names of its contributors may be 50 used to endorse or promote products derived from this software without 51 specific prior written permission. 52 53 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 54 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 55 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 56 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 57 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 58 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 59 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 60 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 61 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 62 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 63 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 64 """ 65 66 # URL: http://tungwaiyip.info/software/HTMLTestRunner.html 67 68 __author__ = "Wai Yip Tung" 69 __version__ = "0.9.1" 70 71 """ 72 Change History 73 Version 0.9.1 74 * 用Echarts添加执行情况统计图 (灰蓝) 75 76 Version 0.9.0 77 * 改成Python 3.x (灰蓝) 78 79 Version 0.8.3 80 * 使用 Bootstrap稍加美化 (灰蓝) 81 * 改为中文 (灰蓝) 82 83 Version 0.8.2 84 * Show output inline instead of popup window (Viorel Lupu). 85 86 Version in 0.8.1 87 * Validated XHTML (Wolfgang Borgert). 88 * Added description of test classes and test cases. 89 90 Version in 0.8.0 91 * Define Template_mixin class for customization. 92 * Workaround a IE 6 bug that it does not treat <script> block as CDATA. 93 94 Version in 0.7.1 95 * Back port to Python 2.3 (Frank Horowitz). 96 * Fix missing scroll bars in detail log (Podi). 97 """ 98 99 # TODO: color stderr 100 # TODO: simplify javascript using ,ore than 1 class in the class attribute? 101 102 import datetime 103 import sys 104 import io 105 import time 106 import unittest 107 from xml.sax import saxutils 108 109 110 # ------------------------------------------------------------------------ 111 # The redirectors below are used to capture output during testing. Output 112 # sent to sys.stdout and sys.stderr are automatically captured. However 113 # in some cases sys.stdout is already cached before HTMLTestRunner is 114 # invoked (e.g. calling logging.basicConfig). In order to capture those 115 # output, use the redirectors for the cached stream. 116 # 117 # e.g. 118 # >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector) 119 # >>> 120 121 class OutputRedirector(object): 122 """ Wrapper to redirect stdout or stderr """ 123 def __init__(self, fp): 124 self.fp = fp 125 126 def write(self, s): 127 self.fp.write(s) 128 129 def writelines(self, lines): 130 self.fp.writelines(lines) 131 132 def flush(self): 133 self.fp.flush() 134 135 stdout_redirector = OutputRedirector(sys.stdout) 136 stderr_redirector = OutputRedirector(sys.stderr) 137 138 139 # ---------------------------------------------------------------------- 140 # Template 141 142 143 class Template_mixin(object): 144 """ 145 Define a HTML template for report customerization and generation. 146 147 Overall structure of an HTML report 148 149 HTML 150 +------------------------+ 151 |<html> | 152 | <head> | 153 | | 154 | STYLESHEET | 155 | +----------------+ | 156 | | | | 157 | +----------------+ | 158 | | 159 | </head> | 160 | | 161 | <body> | 162 | | 163 | HEADING | 164 | +----------------+ | 165 | | | | 166 | +----------------+ | 167 | | 168 | REPORT | 169 | +----------------+ | 170 | | | | 171 | +----------------+ | 172 | | 173 | ENDING | 174 | +----------------+ | 175 | | | | 176 | +----------------+ | 177 | | 178 | </body> | 179 |</html> | 180 +------------------------+ 181 """ 182 183 STATUS = { 184 0: u'通过', 185 1: u'失败', 186 2: u'错误', 187 } 188 189 DEFAULT_TITLE = 'Unit Test Report' 190 DEFAULT_DESCRIPTION = '' 191 192 # ------------------------------------------------------------------------ 193 # HTML Template 194 195 HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?> 196 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 197 <html xmlns="http://www.w3.org/1999/xhtml"> 198 <head> 199 <title>%(title)s</title> 200 <meta name="generator" content="%(generator)s"/> 201 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> 202 203 <link href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet"> 204 <script src="https://cdn.bootcss.com/echarts/3.8.5/echarts.common.min.js"></script> 205 <!-- <script type="text/javascript" src="js/echarts.common.min.js"></script> --> 206 207 %(stylesheet)s 208 209 </head> 210 <body> 211 <script language="javascript" type="text/javascript"><!-- 212 output_list = Array(); 213 214 /* level - 0:Summary; 1:Failed; 2:All */ 215 function showCase(level) { 216 trs = document.getElementsByTagName("tr"); 217 for (var i = 0; i < trs.length; i++) { 218 tr = trs[i]; 219 id = tr.id; 220 if (id.substr(0,2) == 'ft') { 221 if (level < 1) { 222 tr.className = 'hiddenRow'; 223 } 224 else { 225 tr.className = ''; 226 } 227 } 228 if (id.substr(0,2) == 'pt') { 229 if (level > 1) { 230 tr.className = ''; 231 } 232 else { 233 tr.className = 'hiddenRow'; 234 } 235 } 236 } 237 } 238 239 240 function showClassDetail(cid, count) { 241 var id_list = Array(count); 242 var toHide = 1; 243 for (var i = 0; i < count; i++) { 244 tid0 = 't' + cid.substr(1) + '.' + (i+1); 245 tid = 'f' + tid0; 246 tr = document.getElementById(tid); 247 if (!tr) { 248 tid = 'p' + tid0; 249 tr = document.getElementById(tid); 250 } 251 id_list[i] = tid; 252 if (tr.className) { 253 toHide = 0; 254 } 255 } 256 for (var i = 0; i < count; i++) { 257 tid = id_list[i]; 258 if (toHide) { 259 document.getElementById('div_'+tid).style.display = 'none' 260 document.getElementById(tid).className = 'hiddenRow'; 261 } 262 else { 263 document.getElementById(tid).className = ''; 264 } 265 } 266 } 267 268 269 function showTestDetail(div_id){ 270 var details_div = document.getElementById(div_id) 271 var displayState = details_div.style.display 272 // alert(displayState) 273 if (displayState != 'block' ) { 274 displayState = 'block' 275 details_div.style.display = 'block' 276 } 277 else { 278 details_div.style.display = 'none' 279 } 280 } 281 282 283 function html_escape(s) { 284 s = s.replace(/&/g,'&'); 285 s = s.replace(/</g,'<'); 286 s = s.replace(/>/g,'>'); 287 return s; 288 } 289 290 /* obsoleted by detail in <div> 291 function showOutput(id, name) { 292 var w = window.open("", //url 293 name, 294 "resizable,scrollbars,status,width=800,height=450"); 295 d = w.document; 296 d.write("<pre>"); 297 d.write(html_escape(output_list[id])); 298 d.write(" "); 299 d.write("<a href='javascript:window.close()'>close</a> "); 300 d.write("</pre> "); 301 d.close(); 302 } 303 */ 304 --></script> 305 306 <div id="div_base"> 307 %(heading)s 308 %(report)s 309 %(ending)s 310 %(chart_script)s 311 </div> 312 </body> 313 </html> 314 """ # variables: (title, generator, stylesheet, heading, report, ending, chart_script) 315 316 ECHARTS_SCRIPT = """ 317 <script type="text/javascript"> 318 // 基于准备好的dom,初始化echarts实例 319 var myChart = echarts.init(document.getElementById('chart')); 320 321 // 指定图表的配置项和数据 322 var option = { 323 title : { 324 text: '测试执行情况', 325 x:'center' 326 }, 327 tooltip : { 328 trigger: 'item', 329 formatter: "{a} <br/>{b} : {c} ({d}%%)" 330 }, 331 color: ['#95b75d', 'grey', '#b64645'], 332 legend: { 333 orient: 'vertical', 334 left: 'left', 335 data: ['通过','失败','错误'] 336 }, 337 series : [ 338 { 339 name: '测试执行情况', 340 type: 'pie', 341 radius : '60%%', 342 center: ['50%%', '60%%'], 343 data:[ 344 {value:%(Pass)s, name:'通过'}, 345 {value:%(fail)s, name:'失败'}, 346 {value:%(error)s, name:'错误'} 347 ], 348 itemStyle: { 349 emphasis: { 350 shadowBlur: 10, 351 shadowOffsetX: 0, 352 shadowColor: 'rgba(0, 0, 0, 0.5)' 353 } 354 } 355 } 356 ] 357 }; 358 359 // 使用刚指定的配置项和数据显示图表。 360 myChart.setOption(option); 361 </script> 362 """ # variables: (Pass, fail, error) 363 364 # ------------------------------------------------------------------------ 365 # Stylesheet 366 # 367 # alternatively use a <link> for external style sheet, e.g. 368 # <link rel="stylesheet" href="$url" type="text/css"> 369 370 STYLESHEET_TMPL = """ 371 <style type="text/css" media="screen"> 372 body { font-family: Microsoft YaHei,Consolas,arial,sans-serif; font-size: 80%; } 373 table { font-size: 100%; } 374 pre { white-space: pre-wrap;word-wrap: break-word; } 375 376 /* -- heading ---------------------------------------------------------------------- */ 377 h1 { 378 font-size: 16pt; 379 color: gray; 380 } 381 .heading { 382 margin-top: 0ex; 383 margin-bottom: 1ex; 384 } 385 386 .heading .attribute { 387 margin-top: 1ex; 388 margin-bottom: 0; 389 } 390 391 .heading .description { 392 margin-top: 2ex; 393 margin-bottom: 3ex; 394 } 395 396 /* -- css div popup ------------------------------------------------------------------------ */ 397 a.popup_link { 398 } 399 400 a.popup_link:hover { 401 color: red; 402 } 403 404 .popup_window { 405 display: none; 406 position: relative; 407 left: 0px; 408 top: 0px; 409 /*border: solid #627173 1px; */ 410 padding: 10px; 411 /*background-color: #E6E6D6; */ 412 font-family: "Lucida Console", "Courier New", Courier, monospace; 413 text-align: left; 414 font-size: 8pt; 415 /* 500px;*/ 416 } 417 418 } 419 /* -- report ------------------------------------------------------------------------ */ 420 #show_detail_line { 421 margin-top: 3ex; 422 margin-bottom: 1ex; 423 } 424 #result_table { 425 99%; 426 } 427 #header_row { 428 font-weight: bold; 429 color: #303641; 430 background-color: #ebebeb; 431 } 432 #total_row { font-weight: bold; } 433 .passClass { background-color: #bdedbc; } 434 .failClass { background-color: #ffefa4; } 435 .errorClass { background-color: #ffc9c9; } 436 .passCase { color: #6c6; } 437 .failCase { color: #FF6600; font-weight: bold; } 438 .errorCase { color: #c00; font-weight: bold; } 439 .hiddenRow { display: none; } 440 .testcase { margin-left: 2em; } 441 442 443 /* -- ending ---------------------------------------------------------------------- */ 444 #ending { 445 } 446 447 #div_base { 448 position:absolute; 449 top:0%; 450 left:5%; 451 right:5%; 452 auto; 453 height: auto; 454 margin: -15px 0 0 0; 455 } 456 </style> 457 """ 458 459 # ------------------------------------------------------------------------ 460 # Heading 461 # 462 463 HEADING_TMPL = """ 464 <div class='page-header'> 465 <h1>%(title)s</h1> 466 %(parameters)s 467 </div> 468 <div style="float: left;50%%;"><p class='description'>%(description)s</p></div> 469 <div id="chart" style="50%%;height:400px;float:left;"></div> 470 """ # variables: (title, parameters, description) 471 472 HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p> 473 """ # variables: (name, value) 474 475 # ------------------------------------------------------------------------ 476 # Report 477 # 478 479 REPORT_TMPL = u""" 480 <div class="btn-group btn-group-sm"> 481 <button class="btn btn-default" onclick='javascript:showCase(0)'>总结</button> 482 <button class="btn btn-default" onclick='javascript:showCase(1)'>失败</button> 483 <button class="btn btn-default" onclick='javascript:showCase(2)'>全部</button> 484 </div> 485 <p></p> 486 <table id='result_table' class="table table-bordered"> 487 <colgroup> 488 <col align='left' /> 489 <col align='right' /> 490 <col align='right' /> 491 <col align='right' /> 492 <col align='right' /> 493 <col align='right' /> 494 </colgroup> 495 <tr id='header_row'> 496 <td>测试套件/测试用例</td> 497 <td>总数</td> 498 <td>通过</td> 499 <td>失败</td> 500 <td>错误</td> 501 <td>查看</td> 502 </tr> 503 %(test_list)s 504 <tr id='total_row'> 505 <td>总计</td> 506 <td>%(count)s</td> 507 <td>%(Pass)s</td> 508 <td>%(fail)s</td> 509 <td>%(error)s</td> 510 <td> </td> 511 </tr> 512 </table> 513 """ # variables: (test_list, count, Pass, fail, error) 514 515 REPORT_CLASS_TMPL = u""" 516 <tr class='%(style)s'> 517 <td>%(desc)s</td> 518 <td>%(count)s</td> 519 <td>%(Pass)s</td> 520 <td>%(fail)s</td> 521 <td>%(error)s</td> 522 <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">详情</a></td> 523 </tr> 524 """ # variables: (style, desc, count, Pass, fail, error, cid) 525 526 REPORT_TEST_WITH_OUTPUT_TMPL = r""" 527 <tr id='%(tid)s' class='%(Class)s'> 528 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> 529 <td colspan='5' align='center'> 530 531 <!--css div popup start--> 532 <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" > 533 %(status)s</a> 534 535 <div id='div_%(tid)s' class="popup_window"> 536 <pre>%(script)s</pre> 537 </div> 538 <!--css div popup end--> 539 540 </td> 541 </tr> 542 """ # variables: (tid, Class, style, desc, status) 543 544 REPORT_TEST_NO_OUTPUT_TMPL = r""" 545 <tr id='%(tid)s' class='%(Class)s'> 546 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> 547 <td colspan='5' align='center'>%(status)s</td> 548 </tr> 549 """ # variables: (tid, Class, style, desc, status) 550 551 REPORT_TEST_OUTPUT_TMPL = r"""%(id)s: %(output)s""" # variables: (id, output) 552 553 # ------------------------------------------------------------------------ 554 # ENDING 555 # 556 557 ENDING_TMPL = """<div id='ending'> </div>""" 558 559 # -------------------- The end of the Template class ------------------- 560 561 562 TestResult = unittest.TestResult 563 564 565 class _TestResult(TestResult): 566 # note: _TestResult is a pure representation of results. 567 # It lacks the output and reporting ability compares to unittest._TextTestResult. 568 569 def __init__(self, verbosity=1): 570 TestResult.__init__(self) 571 self.stdout0 = None 572 self.stderr0 = None 573 self.success_count = 0 574 self.failure_count = 0 575 self.error_count = 0 576 self.verbosity = verbosity 577 578 # result is a list of result in 4 tuple 579 # ( 580 # result code (0: success; 1: fail; 2: error), 581 # TestCase object, 582 # Test output (byte string), 583 # stack trace, 584 # ) 585 self.result = [] 586 self.subtestlist = [] 587 588 def startTest(self, test): 589 TestResult.startTest(self, test) 590 # just one buffer for both stdout and stderr 591 self.outputBuffer = io.StringIO() 592 stdout_redirector.fp = self.outputBuffer 593 stderr_redirector.fp = self.outputBuffer 594 self.stdout0 = sys.stdout 595 self.stderr0 = sys.stderr 596 sys.stdout = stdout_redirector 597 sys.stderr = stderr_redirector 598 599 def complete_output(self): 600 """ 601 Disconnect output redirection and return buffer. 602 Safe to call multiple times. 603 """ 604 if self.stdout0: 605 sys.stdout = self.stdout0 606 sys.stderr = self.stderr0 607 self.stdout0 = None 608 self.stderr0 = None 609 return self.outputBuffer.getvalue() 610 611 def stopTest(self, test): 612 # Usually one of addSuccess, addError or addFailure would have been called. 613 # But there are some path in unittest that would bypass this. 614 # We must disconnect stdout in stopTest(), which is guaranteed to be called. 615 self.complete_output() 616 617 def addSuccess(self, test): 618 if test not in self.subtestlist: 619 self.success_count += 1 620 TestResult.addSuccess(self, test) 621 output = self.complete_output() 622 self.result.append((0, test, output, '')) 623 if self.verbosity > 1: 624 sys.stderr.write('ok ') 625 sys.stderr.write(str(test)) 626 sys.stderr.write(' ') 627 else: 628 sys.stderr.write('.') 629 630 def addError(self, test, err): 631 self.error_count += 1 632 TestResult.addError(self, test, err) 633 _, _exc_str = self.errors[-1] 634 output = self.complete_output() 635 self.result.append((2, test, output, _exc_str)) 636 if self.verbosity > 1: 637 sys.stderr.write('E ') 638 sys.stderr.write(str(test)) 639 sys.stderr.write(' ') 640 else: 641 sys.stderr.write('E') 642 643 def addFailure(self, test, err): 644 self.failure_count += 1 645 TestResult.addFailure(self, test, err) 646 _, _exc_str = self.failures[-1] 647 output = self.complete_output() 648 self.result.append((1, test, output, _exc_str)) 649 if self.verbosity > 1: 650 sys.stderr.write('F ') 651 sys.stderr.write(str(test)) 652 sys.stderr.write(' ') 653 else: 654 sys.stderr.write('F') 655 656 def addSubTest(self, test, subtest, err): 657 if err is not None: 658 if getattr(self, 'failfast', False): 659 self.stop() 660 if issubclass(err[0], test.failureException): 661 self.failure_count += 1 662 errors = self.failures 663 errors.append((subtest, self._exc_info_to_string(err, subtest))) 664 output = self.complete_output() 665 self.result.append((1, test, output + ' SubTestCase Failed: ' + str(subtest), 666 self._exc_info_to_string(err, subtest))) 667 if self.verbosity > 1: 668 sys.stderr.write('F ') 669 sys.stderr.write(str(subtest)) 670 sys.stderr.write(' ') 671 else: 672 sys.stderr.write('F') 673 else: 674 self.error_count += 1 675 errors = self.errors 676 errors.append((subtest, self._exc_info_to_string(err, subtest))) 677 output = self.complete_output() 678 self.result.append( 679 (2, test, output + ' SubTestCase Error: ' + str(subtest), self._exc_info_to_string(err, subtest))) 680 if self.verbosity > 1: 681 sys.stderr.write('E ') 682 sys.stderr.write(str(subtest)) 683 sys.stderr.write(' ') 684 else: 685 sys.stderr.write('E') 686 self._mirrorOutput = True 687 else: 688 self.subtestlist.append(subtest) 689 self.subtestlist.append(test) 690 self.success_count += 1 691 output = self.complete_output() 692 self.result.append((0, test, output + ' SubTestCase Pass: ' + str(subtest), '')) 693 if self.verbosity > 1: 694 sys.stderr.write('ok ') 695 sys.stderr.write(str(subtest)) 696 sys.stderr.write(' ') 697 else: 698 sys.stderr.write('.') 699 700 701 class HTMLTestRunner(Template_mixin): 702 703 def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None): 704 self.stream = stream 705 self.verbosity = verbosity 706 if title is None: 707 self.title = self.DEFAULT_TITLE 708 else: 709 self.title = title 710 if description is None: 711 self.description = self.DEFAULT_DESCRIPTION 712 else: 713 self.description = description 714 715 self.startTime = datetime.datetime.now() 716 717 def run(self, test): 718 "Run the given test case or test suite." 719 result = _TestResult(self.verbosity) 720 test(result) 721 self.stopTime = datetime.datetime.now() 722 self.generateReport(test, result) 723 print(' Time Elapsed: %s' % (self.stopTime-self.startTime), file=sys.stderr) 724 return result 725 726 def sortResult(self, result_list): 727 # unittest does not seems to run in any particular order. 728 # Here at least we want to group them together by class. 729 rmap = {} 730 classes = [] 731 for n,t,o,e in result_list: 732 cls = t.__class__ 733 if cls not in rmap: 734 rmap[cls] = [] 735 classes.append(cls) 736 rmap[cls].append((n,t,o,e)) 737 r = [(cls, rmap[cls]) for cls in classes] 738 return r 739 740 def getReportAttributes(self, result): 741 """ 742 Return report attributes as a list of (name, value). 743 Override this to add custom attributes. 744 """ 745 startTime = str(self.startTime)[:19] 746 duration = str(self.stopTime - self.startTime) 747 status = [] 748 if result.success_count: status.append(u'通过 %s' % result.success_count) 749 if result.failure_count: status.append(u'失败 %s' % result.failure_count) 750 if result.error_count: status.append(u'错误 %s' % result.error_count ) 751 if status: 752 status = ' '.join(status) 753 else: 754 status = 'none' 755 return [ 756 (u'开始时间', startTime), 757 (u'运行时长', duration), 758 (u'状态', status), 759 ] 760 761 def generateReport(self, test, result): 762 report_attrs = self.getReportAttributes(result) 763 generator = 'HTMLTestRunner %s' % __version__ 764 stylesheet = self._generate_stylesheet() 765 heading = self._generate_heading(report_attrs) 766 report = self._generate_report(result) 767 ending = self._generate_ending() 768 chart = self._generate_chart(result) 769 output = self.HTML_TMPL % dict( 770 title = saxutils.escape(self.title), 771 generator = generator, 772 stylesheet = stylesheet, 773 heading = heading, 774 report = report, 775 ending = ending, 776 chart_script = chart 777 ) 778 self.stream.write(output.encode('utf8')) 779 780 def _generate_stylesheet(self): 781 return self.STYLESHEET_TMPL 782 783 def _generate_heading(self, report_attrs): 784 a_lines = [] 785 for name, value in report_attrs: 786 line = self.HEADING_ATTRIBUTE_TMPL % dict( 787 name = saxutils.escape(name), 788 value = saxutils.escape(value), 789 ) 790 a_lines.append(line) 791 heading = self.HEADING_TMPL % dict( 792 title = saxutils.escape(self.title), 793 parameters = ''.join(a_lines), 794 description = saxutils.escape(self.description), 795 ) 796 return heading 797 798 def _generate_report(self, result): 799 rows = [] 800 sortedResult = self.sortResult(result.result) 801 for cid, (cls, cls_results) in enumerate(sortedResult): 802 # subtotal for a class 803 np = nf = ne = 0 804 for n,t,o,e in cls_results: 805 if n == 0: np += 1 806 elif n == 1: nf += 1 807 else: ne += 1 808 809 # format class description 810 if cls.__module__ == "__main__": 811 name = cls.__name__ 812 else: 813 name = "%s.%s" % (cls.__module__, cls.__name__) 814 doc = cls.__doc__ and cls.__doc__.split(" ")[0] or "" 815 desc = doc and '%s: %s' % (name, doc) or name 816 817 row = self.REPORT_CLASS_TMPL % dict( 818 style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', 819 desc = desc, 820 count = np+nf+ne, 821 Pass = np, 822 fail = nf, 823 error = ne, 824 cid = 'c%s' % (cid+1), 825 ) 826 rows.append(row) 827 828 for tid, (n,t,o,e) in enumerate(cls_results): 829 self._generate_report_test(rows, cid, tid, n, t, o, e) 830 831 report = self.REPORT_TMPL % dict( 832 test_list = ''.join(rows), 833 count = str(result.success_count+result.failure_count+result.error_count), 834 Pass = str(result.success_count), 835 fail = str(result.failure_count), 836 error = str(result.error_count), 837 ) 838 return report 839 840 def _generate_chart(self, result): 841 chart = self.ECHARTS_SCRIPT % dict( 842 Pass=str(result.success_count), 843 fail=str(result.failure_count), 844 error=str(result.error_count), 845 ) 846 return chart 847 848 def _generate_report_test(self, rows, cid, tid, n, t, o, e): 849 # e.g. 'pt1.1', 'ft1.1', etc 850 has_output = bool(o or e) 851 tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1) 852 name = t.id().split('.')[-1] 853 doc = t.shortDescription() or "" 854 desc = doc and ('%s: %s' % (name, doc)) or name 855 tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 856 857 script = self.REPORT_TEST_OUTPUT_TMPL % dict( 858 id=tid, 859 output=saxutils.escape(o+e), 860 ) 861 862 row = tmpl % dict( 863 tid=tid, 864 Class=(n == 0 and 'hiddenRow' or 'none'), 865 style=(n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none')), 866 desc=desc, 867 script=script, 868 status=self.STATUS[n], 869 ) 870 rows.append(row) 871 if not has_output: 872 return 873 874 def _generate_ending(self): 875 return self.ENDING_TMPL 876 877 878 ############################################################################## 879 # Facilities for running tests from the command line 880 ############################################################################## 881 882 # Note: Reuse unittest.TestProgram to launch test. In the future we may 883 # build our own launcher to support more specific command line 884 # parameters like test title, CSS, etc. 885 class TestProgram(unittest.TestProgram): 886 """ 887 A variation of the unittest.TestProgram. Please refer to the base 888 class for command line parameters. 889 """ 890 def runTests(self): 891 # Pick HTMLTestRunner as the default test runner. 892 # base class's testRunner parameter is not useful because it means 893 # we have to instantiate HTMLTestRunner before we know self.verbosity. 894 if self.testRunner is None: 895 self.testRunner = HTMLTestRunner(verbosity=self.verbosity) 896 unittest.TestProgram.runTests(self) 897 898 main = TestProgram 899 900 ############################################################################## 901 # Executing this module from the command line 902 ############################################################################## 903 904 if __name__ == "__main__": 905 main(module=None)
7.2、生成报告调用
1 # 生成报告文件的参数 2 report_title = '接口自动化测试结果' 3 desc = '饼图统计测试执行情况' 4 report_file = basedir + '/report/testsuit.html' 5 with open(report_file, 'wb') as report: 6 runner = HTMLTestRunner(stream=report, title=report_title, description=desc) 7 runner.run(suite)
7.3、报告样式
八、发送邮件
8.1、调用
1 Send_email.cr_zip('TestReport.zip', basedir + '/report/') 2 Send_email.send_mail_report("张雪测试!!!")
8.2、发送邮件的步骤
1-将步骤7中生成的html文件,拿到
2-压缩成.zip文件
3-生成邮件内容
4-将.zip文件放到邮件的附件中
5-发送邮件
8.3、发送邮件的公共方法源码
1 # -*- coding: utf-8 -*- 2 import smtplib 3 import zipfile 4 from email.mime.text import MIMEText #发送纯文本信息 5 from email.mime.multipart import MIMEMultipart #发送带附件的信息 6 from email.header import Header #导入配置库 7 from config.config import basedir 8 import sys 9 from config import config 10 import os 11 12 # 1、压缩文件 13 def cr_zip(outputName, inputPath): 14 # 将inputPath路径下的文件压缩成名字为outputName的文件,放到outputpath目录下 15 16 outputpath = inputPath + outputName 17 filelist = [] 18 isfp = os.path.basename(inputPath) 19 if isfp: 20 print('%s is not path' % inputPath) 21 sys.exit(0) 22 else: 23 for root, subdirs, files in os.walk(inputPath): 24 for file in files: 25 filelist.append(os.path.join(root, file)) 26 27 # 参数1,压缩后的文件夹路径加名字(如果只加name的话,会压缩到调用这个方法的时候的文件路径下); 28 # 参数2,'r' ----- 打开一个存在的只读ZIP文件'w' ----- 清空并打开一个只写的zip文件,或创建一个只写的ZIP文件'a' ----- 表示打开一个文件,并添加内容 29 # 参数3,压缩格式 ,可选的压缩格式只有2个:ZIP_STORE、ZIP_DEFLATED。ZIP_STORE是默认的,表示不压缩。ZIP_DEFLATED表示压缩 30 zf = zipfile.ZipFile(outputpath, 'w', zipfile.ZIP_DEFLATED) 31 for f in filelist: 32 zf.write(f) 33 zf.close() 34 35 # 2、发送邮件 36 def send_mail_report(title): 37 """1、获取测试报告邮件服务器、发件人、收件人、发件人账号密码等信息""" 38 sender = config.sender #发件人 39 receiver = config.receiver #收件人 40 #第三方SMTP服务 41 server = config.server #设置服务器 42 username = config.emailusername #用户名 43 password = config.emailpassword #口令 44 45 """2、获取最新测试报告""" 46 reportPath=config.basedir+"/report/" 47 newReport = "" 48 for root, subdirs, files in os.walk(reportPath): 49 for file in files: 50 if os.path.splitext(file)[1] == ".html": # 判断该目录下的文件扩展名是否为html 51 newReport=file 52 53 """2.1调用cr_zip()方法,将测试报告压缩一下。""" 54 cr_zip('TestReport.zip', basedir + '/report/') 55 56 """3、生成邮件的内容""" 57 msg = MIMEMultipart() #MIMEMultipart(),创建一个带附件的实例 58 msg["subject"] = title #"""邮件需要三个头部信息: From, To, 和 Subject""" 59 msg["from"] = Header(config.sender,'utf-8') 60 msg["to"] = Header(",".join(config.receiver),'utf-8') 61 with open(os.path.join(reportPath,newReport), 'rb') as f: 62 mailbody = f.read() 63 html = MIMEText(mailbody, _subtype='html', _charset='utf-8') 64 msg.attach(html) 65 66 """4、将测试报告压缩文件添加到邮件附件""" 67 68 att = MIMEText(open(basedir + '/report/' + 'TestReport.zip', 'rb').read(), 'base64', 'utf-8') 69 att["Content-Type"] = 'application/octet-stream' #application/octet-stream : 二进制流数据(如常见的文件下载) 70 att.add_header("Content-Disposition", "attachment", filename="TestReport.zip") #filename为附件名 71 msg.attach(att) 72 73 """5、发送邮件""" 74 try: 75 s = smtplib.SMTP(server, 25) #25 为 SMTP 端口号 76 """s.set_debuglevel(1)认证""" 77 s.login(username, password) 78 """发送邮件""" 79 s.sendmail(sender, receiver, msg.as_string()) 80 s.quit() 81 print("邮件发送成功") 82 except smtplib.SMTPException: 83 print("Error :无法发送邮件") 84 85 86 if __name__ =='__main__': 87 cr_zip('TestReport.zip',basedir + '/report/') 88 print(basedir+'/report/') 89 send_mail_report("接口测试报告")
8.4、发送到邮箱的样式
九、结束啦
以上就是简约版本的整个项目的功能,常用的基本的都包含了,涉及到哪些具体模块的,大家可以百度查阅详细的信息,也可以私聊一起探讨。
如果对大家有帮助的,记得点赞支持哈~~