Python+unittest+requests+htmlTestRunner+excel完整的接口自動化框架


一、功能介紹

目前實現的功能有對接口進行測試(類里面進行用例編寫)、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,功能更加強大,以后會介紹到。

 

三、項目目錄 

 

 

 

四、依賴的環境

因為這個項目搭建的太久了,可能會漏掉一些包,所以直接把當前所有的包都導出來了,可以直接都下載,也可以選擇哪些用得到的下載

 

pip3 install -r requirements.txt
 
 
requirements.txt
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,'&amp;');
285         s = s.replace(/</g,'&lt;');
286         s = s.replace(/>/g,'&gt;');
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("\n");
299         d.write("<a href='javascript:window.close()'>close</a>\n");
300         d.write("</pre>\n");
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         /* width: 500px;*/
416     }
417 
418     }
419     /* -- report ------------------------------------------------------------------------ */
420     #show_detail_line {
421         margin-top: 3ex;
422         margin-bottom: 1ex;
423     }
424     #result_table {
425         width: 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                 width: 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;width:50%%;"><p class='description'>%(description)s</p></div>
469     <div id="chart" style="width: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>&nbsp;</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'>&nbsp;</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('\n')
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('\n')
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('\n')
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 + '\nSubTestCase Failed:\n' + 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('\n')
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 + '\nSubTestCase Error:\n' + 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('\n')
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 + '\nSubTestCase Pass:\n' + str(subtest), ''))
693             if self.verbosity > 1:
694                 sys.stderr.write('ok ')
695                 sys.stderr.write(str(subtest))
696                 sys.stderr.write('\n')
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('\nTime 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("\n")[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)
View Code

 

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、發送到郵箱的樣式

 

 

 九、結束啦

以上就是簡約版本的整個項目的功能,常用的基本的都包含了,涉及到哪些具體模塊的,大家可以百度查閱詳細的信息,也可以私聊一起探討。

如果對大家有幫助的,記得點贊支持哈~~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM