Python+unittest+requests 介面自動化測試框架搭建教程
一、Python+unittest+requests+HTMLTestRunner 完整的介面自動化測試框架搭建_00——框架結構簡解
首先配置好開發環境,下載安裝Python並下載安裝pycharm,在pycharm中建立專案功能目錄。如果不會的可以百度Google一下,該內容網上的講解還是比較多比較全的!
大家可以先簡單瞭解下該專案的目錄結構介紹,後面會針對每個檔案有詳細註解和程式碼。
common:
——configDb.py:這個檔案主要編寫資料庫連線池的相關內容,本專案暫未考慮使用資料庫來儲存讀取資料,此檔案可忽略,或者不建立。本人是留著以後如果有相關操作時,方便使用。
——configEmail.py
——configHttp.py:這個檔案主要來通過get、post、put、delete等方法來進行http請求,並拿到請求響應。
——HTMLTestRunner.py:主要是生成測試報告相關
——Log.py:呼叫該類的方法,用來列印生成日誌
result:
——logs:生成的日誌檔案
——report.html:生成的測試報告
testCase:
——test01case.py:讀取userCase.xlsx中的用例,使用unittest來進行斷言校驗
testFile/case:
——userCase.xlsx
caselist.txt:配置將要執行testCase目錄下的哪些用例檔案,前加#代表不進行執行。當專案過於龐大,用例足夠多的時候,我們可以通過這個開關,來確定本次執行哪些介面的哪些用例。
config.ini:資料庫、郵箱、介面等的配置項,用於方便的呼叫讀取。
getpathInfo.py:獲取專案絕對路徑
geturlParams.py:獲取介面的URL、引數、method等
readConfig.py:讀取配置檔案的方法,並返回檔案中內容
readExcel.py:讀取Excel的方法
runAll.py:開始執行介面自動化,專案工程部署完畢後直接執行該檔案即可
test_api.py:自己寫的提供本地測試的介面服務
test_sql.py:測試資料庫連線池的檔案,本次專案未用到資料庫,可以忽略
二、Python+unittest+requests+HTMLTestRunner完整的介面自動化測試框架搭建_01——測試介面服務
首先,我們想搭建一個介面自動化測試框架,前提我們必須要有一個可支援測試的介面服務。有人可能會說,現在我們的環境不管測試環境,還是生產環境有現成的介面。但是,一般工作環境中的介面,不太滿足我們框架的各種條件。舉例如,介面a可能是get介面b可能又是post,等等等等。因此我決定自己寫一個簡單的介面!用於我們這個框架的測試!
按第一講的目錄建立好檔案,開啟test_api.py,寫入如下程式碼
import flask import json from flask import request ''' flask: web框架,通過flask提供的裝飾器@server.route()將普通函式轉換為服 ''' # 建立一個服務,把當前這個python檔案當做一個服務 server = flask.Flask(__name__) # @server.route()可以將普通函式轉變為服務 登入介面的路徑、請求方式 @server.route('/login',methods=['get','post']) def login(): # 獲取通過url請求傳參的資料 username = request.values.get('name') # 獲取url請求傳的密碼,明文 pwd = request.values.get('pwd') # 判斷使用者名稱、密碼都不為空 if username and pwd: if username == 'xiaoming' and pwd == '111': resu = {'code': 200,'message': '登入成功'} return json.dumps(resu,ensure_ascii=False) # 將字典轉換字串 else: resu = {'code': -1,'message': '賬號密碼錯誤'} return json.dumps(resu,ensure_ascii=False) else: resu = {'code': 10001,'message': '引數不能為空!'} return json.dumps(resu,ensure_ascii=False) if __name__ == '__main__': server.run(debug=True,port=8888,host='127.0.0.1')
執行test_api.py,在瀏覽器中輸入http://127.0.0.1:8888/login?name=xiaoming&pwd=11199回車,驗證我們的介面服務是否正常~
變更我們的引數,檢視不同的響應結果確認介面服務一切正常
三、Python+unittest+requests+HTMLTestRunner完整的介面自動化測試框架搭建_02——配置檔案讀取
在我們第二講中,我們已經通過flask這個web框架建立好了我們用於測試的介面服務,因此我們可以把這個介面抽出來一些引數放到配置檔案,然後通過一個讀取配置檔案的方法,方便後續的使用。同樣還有郵件的相關配置~
按第一講的目錄建立好config.ini檔案,開啟該檔案寫入如下:
# -*- coding: utf-8 -*- [HTTP] scheme = http baseurl = 127.0.0.1 port = 8888 timeout = 10.0 [EMAIL] on_off = on; subject = 介面自動化測試報告 app = Outlook addressee = [email protected] cc = [email protected]
在HTTP中,協議http,baseURL,埠,超時時間。
在郵件中on_off是設定的一個開關,=on開啟,傳送郵件,=其他不傳送郵件。subject郵件主題,addressee收件人,cc抄送人。
在我們編寫readConfig.py檔案前,我們先寫一個獲取專案某路徑下某檔案絕對路徑的一個方法。按第一講的目錄結構建立好getpathInfo.py,開啟該檔案
import os def get_Path(): path = os.path.split(os.path.realpath(__file__))[0] return path if __name__ == '__main__':# 執行該檔案,測試下是否OK print('測試路徑是否OK,路徑為:',get_Path())
填寫如上程式碼並執行後,檢視輸出結果,打印出了該專案的絕對路徑:
繼續往下走,同理,按第一講目錄建立好readConfig.py檔案,開啟該檔案,以後的章節不在累贅
import os import configparser import getpathInfo#引入我們自己的寫的獲取路徑的類 path = getpathInfo.get_Path()#呼叫例項化,還記得這個類返回的路徑為C:\Users\songlihui\PycharmProjects\dkxinterfaceTest config_path = os.path.join(path,'config.ini')#這句話是在path路徑下再加一級,最後變成C:\Users\songlihui\PycharmProjects\dkxinterfaceTest\config.ini config = configparser.ConfigParser()#呼叫外部的讀取配置檔案的方法 config.read(config_path,encoding='utf-8') class ReadConfig(): def get_http(self,name): value = config.get('HTTP',name) return value def get_email(self,name): value = config.get('EMAIL',name) return value def get_mysql(self,name):#寫好,留以後備用。但是因為我們沒有對資料庫的操作,所以這個可以遮蔽掉 value = config.get('DATABASE',name) return value if __name__ == '__main__':#測試一下,我們讀取配置檔案的方法是否可用 print('HTTP中的baseurl值為:',ReadConfig().get_http('baseurl')) print('EMAIL中的開關on_off值為:',ReadConfig().get_email('on_off'))
執行下readConfig.py,檢視資料是否正確
一切OK
四、Python+unittest+requests+HTMLTestRunner完整的介面自動化測試框架搭建_03——讀取Excel中的case
配置檔案寫好了,介面我們也有了,然後我們來根據我們的介面設計我們簡單的幾條用例。首先在前兩講中我們寫了一個我們測試的介面服務,針對這個介面服務存在三種情況的校驗。正確的使用者名稱和密碼,賬號密碼錯誤和賬號密碼為空
我們根據上面的三種情況,將對這個介面的用例寫在一個對應的單獨檔案中testFile\case\userCase.xlsx,userCase.xlsx內容如下:
緊接著,我們有了用例設計的Excel了,我們要對這個Excel進行資料的讀取操作,繼續往下,我們建立readExcel.py檔案
import os import getpathInfo# 自己定義的內部類,該類返回專案的絕對路徑 #呼叫讀Excel的第三方庫xlrd from xlrd import open_workbook # 拿到該專案所在的絕對路徑 path = getpathInfo.get_Path() class readExcel(): def get_xls(self,xls_name,sheet_name):# xls_name填寫用例的Excel名稱 sheet_name該Excel的sheet名稱 cls = [] # 獲取用例檔案路徑 xlsPath = os.path.join(path,"testFile",'case',xls_name) file = open_workbook(xlsPath)# 開啟用例Excel sheet = file.sheet_by_name(sheet_name)#獲得開啟Excel的sheet # 獲取這個sheet內容行數 nrows = sheet.nrows for i in range(nrows):#根據行數做迴圈 if sheet.row_values(i)[0] != u'case_name':#如果這個Excel的這個sheet的第i行的第一列不等於case_name那麼我們把這行的資料新增到cls[] cls.append(sheet.row_values(i)) return cls if __name__ == '__main__':#我們執行該檔案測試一下是否可以正確獲取Excel中的值 print(readExcel().get_xls('userCase.xlsx','login')) print(readExcel().get_xls('userCase.xlsx','login')[0][1]) print(readExcel().get_xls('userCase.xlsx','login')[1][2])
結果為:
完全正確~
五、Python+unittest+requests+HTMLTestRunner完整的介面自動化測試框架搭建_04——requests請求
配置檔案有了,讀取配置檔案有了,用例有了,讀取用例有了,我們的介面服務有了,我們是不是該寫對某個介面進行http請求了,這時候我們需要使用pip install requests來安裝第三方庫,在common下configHttp.py,configHttp.py的內容如下:
import requests import json class RunMain(): def send_post(self,url,data): # 定義一個方法,傳入需要的引數url和data # 引數必須按照url、data順序傳入 result = requests.post(url=url,data=data).json() # 因為這裡要封裝post方法,所以這裡的url和data值不能寫死 res = json.dumps(result,ensure_ascii=False,sort_keys=True,indent=2) return res def send_get(self,data): result = requests.get(url=url,params=data).json() res = json.dumps(result,indent=2) return res def run_main(self,method,url=None,data=None): # 定義一個run_main函式,通過傳過來的method來進行不同的get或post請求 result = None if method == 'post': result = self.send_post(url,data) elif method == 'get': result = self.send_get(url,data) else: print("method值錯誤!!!") return result if __name__ == '__main__': # 通過寫死引數,來驗證我們寫的請求是否正確 result1 = RunMain().run_main('post','http://127.0.0.1:8888/login',{'name': 'xiaoming','pwd':'111'}) result2 = RunMain().run_main('get','name=xiaoming&pwd=111') print(result1) print(result2)
執行該檔案,驗證結果正確性:
我們發現和瀏覽器中進行請求該介面,得到的結果一致,說明沒有問題,一切OK
六、Python+unittest+requests+HTMLTestRunner完整的介面自動化測試框架搭建_05——引數動態化
在上一講中,我們寫了針對我們的介面服務,設計的三種測試用例,使用寫死的引數(result = RunMain().run_main('post','name=xiaoming&pwd='))來進行requests請求。本講中我們寫一個類,來用於分別獲取這些引數,來第一講的目錄建立geturlParams.py,geturlParams.py檔案中的內容如下:
import readConfig as readConfig readconfig = readConfig.ReadConfig() class geturlParams():# 定義一個方法,將從配置檔案中讀取的進行拼接 def get_Url(self): new_url = readconfig.get_http('scheme') + '://' + readconfig.get_http('baseurl') + ':8888' + '/login' + '?' #logger.info('new_url'+new_url) return new_url if __name__ == '__main__':# 驗證拼接後的正確性 print(geturlParams().get_Url())
通過將配置檔案中的進行拼接,拼接後的結果:http://127.0.0.1:8888/login?和我們請求的一致
七、Python+unittest+requests+HTMLTestRunner完整的介面自動化測試框架搭建_06——unittest斷言
以上的我們都準備好了,剩下的該寫我們的unittest斷言測試case了,在testCase下建立test01case.py檔案,檔案中內容如下:
import json import unittest from common.configHttp import RunMain import paramunittest import geturlParams import urllib.parse # import pythoncom import readExcel # pythoncom.CoInitialize() url = geturlParams.geturlParams().get_Url()# 呼叫我們的geturlParams獲取我們拼接的URL login_xls = readExcel.readExcel().get_xls('userCase.xlsx','login') @paramunittest.parametrized(*login_xls) class testUserLogin(unittest.TestCase): def setParameters(self,case_name,path,query,method): """ set params :param case_name: :param path :param query :param method :return: """ self.case_name = str(case_name) self.path = str(path) self.query = str(query) self.method = str(method) def description(self): """ test report description :return: """ self.case_name def setUp(self): """ :return: """ print(self.case_name+"測試開始前準備") def test01case(self): self.checkResult() def tearDown(self): print("測試結束,輸出log完結\n\n") def checkResult(self):# 斷言 """ check test result :return: """ url1 = "http://www.xxx.com/login?" new_url = url1 + self.query data1 = dict(urllib.parse.parse_qsl(urllib.parse.urlsplit(new_url).query))# 將一個完整的URL中的name=&pwd=轉換為{'name':'xxx','pwd':'bbb'} info = RunMain().run_main(self.method,data1)# 根據Excel中的method呼叫run_main來進行requests請求,並拿到響應 ss = json.loads(info)# 將響應轉換為字典格式 if self.case_name == 'login':# 如果case_name是login,說明合法,返回的code應該為200 self.assertEqual(ss['code'],200) if self.case_name == 'login_error':# 同上 self.assertEqual(ss['code'],-1) if self.case_name == 'login_null':# 同上 self.assertEqual(ss['code'],10001)
八、Python+unittest+requests+HTMLTestRunner完整的介面自動化測試框架搭建_07——HTMLTestRunner
按我的目錄結構,在common下建立HTMLTestRunner.py檔案,內容如下:
# -*- coding: utf-8 -*- """ A TestRunner for use with the Python unit testing framework. It generates a HTML report to show the result at a glance. The simplest way to use this is to invoke its main method. E.g. import unittest import HTMLTestRunner ... define your tests ... if __name__ == '__main__': HTMLTestRunner.main() For more customization options,instantiates a HTMLTestRunner object. HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g. # output to a file fp = file('my_report.html','wb') runner = HTMLTestRunner.HTMLTestRunner( stream=fp,title='My unit test',description='This demonstrates the report output by HTMLTestRunner.' ) # Use an external stylesheet. # See the Template_mixin class for more customizable options runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">' # run the test runner.run(my_test_suite) ------------------------------------------------------------------------ Copyright (c) 2004-2007,Wai Yip Tung All rights reserved. Redistribution and use in source and binary forms,with or without modification,are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice,this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice,this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Wai Yip Tung nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,INCLUDING,BUT NOT LIMITED TO,THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,INDIRECT,INCIDENTAL,SPECIAL,EXEMPLARY,OR CONSEQUENTIAL DAMAGES (INCLUDING,BUT NOT LIMITED TO,PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,DATA,OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,WHETHER IN CONTRACT,STRICT LIABILITY,OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ # URL: http://tungwaiyip.info/software/HTMLTestRunner.html __author__ = "Wai Yip Tung" __version__ = "0.9.1" """ Change History Version 0.9.1 * 用Echarts新增執行情況統計圖 (灰藍) Version 0.9.0 * 改成Python 3.x (灰藍) Version 0.8.3 * 使用 Bootstrap稍加美化 (灰藍) * 改為中文 (灰藍) Version 0.8.2 * Show output inline instead of popup window (Viorel Lupu). Version in 0.8.1 * Validated XHTML (Wolfgang Borgert). * Added description of test classes and test cases. Version in 0.8.0 * Define Template_mixin class for customization. * Workaround a IE 6 bug that it does not treat <script> block as CDATA. Version in 0.7.1 * Back port to Python 2.3 (Frank Horowitz). * Fix missing scroll bars in detail log (Podi). """ # TODO: color stderr # TODO: simplify javascript using,ore than 1 class in the class attribute? import datetime import sys import io import time import unittest from xml.sax import saxutils # ------------------------------------------------------------------------ # The redirectors below are used to capture output during testing. Output # sent to sys.stdout and sys.stderr are automatically captured. However # in some cases sys.stdout is already cached before HTMLTestRunner is # invoked (e.g. calling logging.basicConfig). In order to capture those # output,use the redirectors for the cached stream. # # e.g. # >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector) # >>> class OutputRedirector(object): """ Wrapper to redirect stdout or stderr """ def __init__(self,fp): self.fp = fp def write(self,s): self.fp.write(s) def writelines(self,lines): self.fp.writelines(lines) def flush(self): self.fp.flush() stdout_redirector = OutputRedirector(sys.stdout) stderr_redirector = OutputRedirector(sys.stderr) # ---------------------------------------------------------------------- # Template class Template_mixin(object): """ Define a HTML template for report customerization and generation. Overall structure of an HTML report HTML +------------------------+ |<html> | | <head> | | | | STYLESHEET | | +----------------+ | | | | | | +----------------+ | | | | </head> | | | | <body> | | | | HEADING | | +----------------+ | | | | | | +----------------+ | | | | REPORT | | +----------------+ | | | | | | +----------------+ | | | | ENDING | | +----------------+ | | | | | | +----------------+ | | | | </body> | |</html> | +------------------------+ """ STATUS = { 0: u'通過',1: u'失敗',2: u'錯誤',} DEFAULT_TITLE = 'Unit Test Report' DEFAULT_DESCRIPTION = '' # ------------------------------------------------------------------------ # HTML Template HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>%(title)s</title> <meta name="generator" content="%(generator)s"/> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <link href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet"> <script src="https://cdn.bootcss.com/echarts/3.8.5/echarts.common.min.js"></script> <!-- <script type="text/javascript" src="js/echarts.common.min.js"></script> --> %(stylesheet)s </head> <body> <script language="javascript" type="text/javascript"><!-- output_list = Array(); /* level - 0:Summary; 1:Failed; 2:All */ function showCase(level) { trs = document.getElementsByTagName("tr"); for (var i = 0; i < trs.length; i++) { tr = trs[i]; id = tr.id; if (id.substr(0,2) == 'ft') { if (level < 1) { tr.className = 'hiddenRow'; } else { tr.className = ''; } } if (id.substr(0,2) == 'pt') { if (level > 1) { tr.className = ''; } else { tr.className = 'hiddenRow'; } } } } function showClassDetail(cid,count) { var id_list = Array(count); var toHide = 1; for (var i = 0; i < count; i++) { tid0 = 't' + cid.substr(1) + '.' + (i+1); tid = 'f' + tid0; tr = document.getElementById(tid); if (!tr) { tid = 'p' + tid0; tr = document.getElementById(tid); } id_list[i] = tid; if (tr.className) { toHide = 0; } } for (var i = 0; i < count; i++) { tid = id_list[i]; if (toHide) { document.getElementById('div_'+tid).style.display = 'none' document.getElementById(tid).className = 'hiddenRow'; } else { document.getElementById(tid).className = ''; } } } function showTestDetail(div_id){ var details_div = document.getElementById(div_id) var displayState = details_div.style.display // alert(displayState) if (displayState != 'block' ) { displayState = 'block' details_div.style.display = 'block' } else { details_div.style.display = 'none' } } function html_escape(s) { s = s.replace(/&/g,'&'); s = s.replace(/</g,'<'); s = s.replace(/>/g,'>'); return s; } /* obsoleted by detail in <div> function showOutput(id,name) { var w = window.open("",//url name,"resizable,scrollbars,status,width=800,height=450"); d = w.document; d.write("<pre>"); d.write(html_escape(output_list[id])); d.write("\n"); d.write("<a href='javascript:window.close()'>close</a>\n"); d.write("</pre>\n"); d.close(); } */ --></script> <div id="div_base"> %(heading)s %(report)s %(ending)s %(chart_script)s </div> </body> </html> """ # variables: (title,generator,stylesheet,heading,report,ending,chart_script) ECHARTS_SCRIPT = """ <script type="text/javascript"> // 基於準備好的dom,初始化echarts例項 var myChart = echarts.init(document.getElementById('chart')); // 指定圖表的配置項和資料 var option = { title : { text: '測試執行情況',x:'center' },tooltip : { trigger: 'item',formatter: "{a} <br/>{b} : {c} ({d}%%)" },color: ['#95b75d','grey','#b64645'],legend: { orient: 'vertical',left: 'left',data: ['通過','失敗','錯誤'] },series : [ { name: '測試執行情況',type: 'pie',radius : '60%%',center: ['50%%','60%%'],data:[ {value:%(Pass)s,name:'通過'},{value:%(fail)s,name:'失敗'},{value:%(error)s,name:'錯誤'} ],itemStyle: { emphasis: { shadowBlur: 10,shadowOffsetX: 0,shadowColor: 'rgba(0,0.5)' } } } ] }; // 使用剛指定的配置項和資料顯示圖表。 myChart.setOption(option); </script> """ # variables: (Pass,fail,error) # ------------------------------------------------------------------------ # Stylesheet # # alternatively use a <link> for external style sheet,e.g. # <link rel="stylesheet" href="$url" type="text/css"> STYLESHEET_TMPL = """ <style type="text/css" media="screen"> body { font-family: Microsoft YaHei,Consolas,arial,sans-serif; font-size: 80%; } table { font-size: 100%; } pre { white-space: pre-wrap;word-wrap: break-word; } /* -- heading ---------------------------------------------------------------------- */ h1 { font-size: 16pt; color: gray; } .heading { margin-top: 0ex; margin-bottom: 1ex; } .heading .attribute { margin-top: 1ex; margin-bottom: 0; } .heading .description { margin-top: 2ex; margin-bottom: 3ex; } /* -- css div popup ------------------------------------------------------------------------ */ a.popup_link { } a.popup_link:hover { color: red; } .popup_window { display: none; position: relative; left: 0px; top: 0px; /*border: solid #627173 1px; */ padding: 10px; /*background-color: #E6E6D6; */ font-family: "Lucida Console","Courier New",Courier,monospace; text-align: left; font-size: 8pt; /* width: 500px;*/ } } /* -- report ------------------------------------------------------------------------ */ #show_detail_line { margin-top: 3ex; margin-bottom: 1ex; } #result_table { width: 99%; } #header_row { font-weight: bold; color: #303641; background-color: #ebebeb; } #total_row { font-weight: bold; } .passClass { background-color: #bdedbc; } .failClass { background-color: #ffefa4; } .errorClass { background-color: #ffc9c9; } .passCase { color: #6c6; } .failCase { color: #FF6600; font-weight: bold; } .errorCase { color: #c00; font-weight: bold; } .hiddenRow { display: none; } .testcase { margin-left: 2em; } /* -- ending ---------------------------------------------------------------------- */ #ending { } #div_base { position:absolute; top:0%; left:5%; right:5%; width: auto; height: auto; margin: -15px 0 0 0; } </style> """ # ------------------------------------------------------------------------ # Heading # HEADING_TMPL = """ <div class='page-header'> <h1>%(title)s</h1> %(parameters)s </div> <div style="float: left;width:50%%;"><p class='description'>%(description)s</p></div> <div id="chart" style="width:50%%;height:400px;float:left;"></div> """ # variables: (title,parameters,description) HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p> """ # variables: (name,value) # ------------------------------------------------------------------------ # Report # REPORT_TMPL = u""" <div class="btn-group btn-group-sm"> <button class="btn btn-default" onclick='javascript:showCase(0)'>總結</button> <button class="btn btn-default" onclick='javascript:showCase(1)'>失敗</button> <button class="btn btn-default" onclick='javascript:showCase(2)'>全部</button> </div> <p></p> <table id='result_table' class="table table-bordered"> <colgroup> <col align='left' /> <col align='right' /> <col align='right' /> <col align='right' /> <col align='right' /> <col align='right' /> </colgroup> <tr id='header_row'> <td>測試套件/測試用例</td> <td>總數</td> <td>通過</td> <td>失敗</td> <td>錯誤</td> <td>檢視</td> </tr> %(test_list)s <tr id='total_row'> <td>總計</td> <td>%(count)s</td> <td>%(Pass)s</td> <td>%(fail)s</td> <td>%(error)s</td> <td> </td> </tr> </table> """ # variables: (test_list,count,Pass,error) REPORT_CLASS_TMPL = u""" <tr class='%(style)s'> <td>%(desc)s</td> <td>%(count)s</td> <td>%(Pass)s</td> <td>%(fail)s</td> <td>%(error)s</td> <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">詳情</a></td> </tr> """ # variables: (style,desc,error,cid) REPORT_TEST_WITH_OUTPUT_TMPL = r""" <tr id='%(tid)s' class='%(Class)s'> <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> <td colspan='5' align='center'> <!--css div popup start--> <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" > %(status)s</a> <div id='div_%(tid)s' class="popup_window"> <pre>%(script)s</pre> </div> <!--css div popup end--> </td> </tr> """ # variables: (tid,Class,style,status) REPORT_TEST_NO_OUTPUT_TMPL = r""" <tr id='%(tid)s' class='%(Class)s'> <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> <td colspan='5' align='center'>%(status)s</td> </tr> """ # variables: (tid,status) REPORT_TEST_OUTPUT_TMPL = r"""%(id)s: %(output)s""" # variables: (id,output) # ------------------------------------------------------------------------ # ENDING # ENDING_TMPL = """<div id='ending'> </div>""" # -------------------- The end of the Template class ------------------- TestResult = unittest.TestResult class _TestResult(TestResult): # note: _TestResult is a pure representation of results. # It lacks the output and reporting ability compares to unittest._TextTestResult. def __init__(self,verbosity=1): TestResult.__init__(self) self.stdout0 = None self.stderr0 = None self.success_count = 0 self.failure_count = 0 self.error_count = 0 self.verbosity = verbosity # result is a list of result in 4 tuple # ( # result code (0: success; 1: fail; 2: error),# TestCase object,# Test output (byte string),# stack trace,# ) self.result = [] self.subtestlist = [] def startTest(self,test): TestResult.startTest(self,test) # just one buffer for both stdout and stderr self.outputBuffer = io.StringIO() stdout_redirector.fp = self.outputBuffer stderr_redirector.fp = self.outputBuffer self.stdout0 = sys.stdout self.stderr0 = sys.stderr sys.stdout = stdout_redirector sys.stderr = stderr_redirector def complete_output(self): """ Disconnect output redirection and return buffer. Safe to call multiple times. """ if self.stdout0: sys.stdout = self.stdout0 sys.stderr = self.stderr0 self.stdout0 = None self.stderr0 = None return self.outputBuffer.getvalue() def stopTest(self,test): # Usually one of addSuccess,addError or addFailure would have been called. # But there are some path in unittest that would bypass this. # We must disconnect stdout in stopTest(),which is guaranteed to be called. self.complete_output() def addSuccess(self,test): if test not in self.subtestlist: self.success_count += 1 TestResult.addSuccess(self,test) output = self.complete_output() self.result.append((0,test,output,'')) if self.verbosity > 1: sys.stderr.write('ok ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('.') def addError(self,err): self.error_count += 1 TestResult.addError(self,err) _,_exc_str = self.errors[-1] output = self.complete_output() self.result.append((2,_exc_str)) if self.verbosity > 1: sys.stderr.write('E ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('E') def addFailure(self,err): self.failure_count += 1 TestResult.addFailure(self,_exc_str = self.failures[-1] output = self.complete_output() self.result.append((1,_exc_str)) if self.verbosity > 1: sys.stderr.write('F ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('F') def addSubTest(self,subtest,err): if err is not None: if getattr(self,'failfast',False): self.stop() if issubclass(err[0],test.failureException): self.failure_count += 1 errors = self.failures errors.append((subtest,self._exc_info_to_string(err,subtest))) output = self.complete_output() self.result.append((1,output + '\nSubTestCase Failed:\n' + str(subtest),subtest))) if self.verbosity > 1: sys.stderr.write('F ') sys.stderr.write(str(subtest)) sys.stderr.write('\n') else: sys.stderr.write('F') else: self.error_count += 1 errors = self.errors errors.append((subtest,subtest))) output = self.complete_output() self.result.append( (2,output + '\nSubTestCase Error:\n' + str(subtest),subtest))) if self.verbosity > 1: sys.stderr.write('E ') sys.stderr.write(str(subtest)) sys.stderr.write('\n') else: sys.stderr.write('E') self._mirrorOutput = True else: self.subtestlist.append(subtest) self.subtestlist.append(test) self.success_count += 1 output = self.complete_output() self.result.append((0,output + '\nSubTestCase Pass:\n' + str(subtest),'')) if self.verbosity > 1: sys.stderr.write('ok ') sys.stderr.write(str(subtest)) sys.stderr.write('\n') else: sys.stderr.write('.') class HTMLTestRunner(Template_mixin): def __init__(self,stream=sys.stdout,verbosity=1,title=None,description=None): self.stream = stream self.verbosity = verbosity if title is None: self.title = self.DEFAULT_TITLE else: self.title = title if description is None: self.description = self.DEFAULT_DESCRIPTION else: self.description = description self.startTime = datetime.datetime.now() def run(self,test): "Run the given test case or test suite." result = _TestResult(self.verbosity) test(result) self.stopTime = datetime.datetime.now() self.generateReport(test,result) print('\nTime Elapsed: %s' % (self.stopTime - self.startTime),file=sys.stderr) return result def sortResult(self,result_list): # unittest does not seems to run in any particular order. # Here at least we want to group them together by class. rmap = {} classes = [] for n,t,o,e in result_list: cls = t.__class__ if cls not in rmap: rmap[cls] = [] classes.append(cls) rmap[cls].append((n,e)) r = [(cls,rmap[cls]) for cls in classes] return r def getReportAttributes(self,result): """ Return report attributes as a list of (name,value). Override this to add custom attributes. """ startTime = str(self.startTime)[:19] duration = str(self.stopTime - self.startTime) status = [] if result.success_count: status.append(u'通過 %s' % result.success_count) if result.failure_count: status.append(u'失敗 %s' % result.failure_count) if result.error_count: status.append(u'錯誤 %s' % result.error_count) if status: status = ' '.join(status) else: status = 'none' return [ (u'開始時間',startTime),(u'執行時長',duration),(u'狀態',status),] def generateReport(self,result): report_attrs = self.getReportAttributes(result) generator = 'HTMLTestRunner %s' % __version__ stylesheet = self._generate_stylesheet() heading = self._generate_heading(report_attrs) report = self._generate_report(result) ending = self._generate_ending() chart = self._generate_chart(result) output = self.HTML_TMPL % dict( title=saxutils.escape(self.title),generator=generator,stylesheet=stylesheet,heading=heading,report=report,ending=ending,chart_script=chart ) self.stream.write(output.encode('utf8')) def _generate_stylesheet(self): return self.STYLESHEET_TMPL def _generate_heading(self,report_attrs): a_lines = [] for name,value in report_attrs: line = self.HEADING_ATTRIBUTE_TMPL % dict( name=saxutils.escape(name),value=saxutils.escape(value),) a_lines.append(line) heading = self.HEADING_TMPL % dict( title=saxutils.escape(self.title),parameters=''.join(a_lines),description=saxutils.escape(self.description),) return heading def _generate_report(self,result): rows = [] sortedResult = self.sortResult(result.result) for cid,(cls,cls_results) in enumerate(sortedResult): # subtotal for a class np = nf = ne = 0 for n,e in cls_results: if n == 0: np += 1 elif n == 1: nf += 1 else: ne += 1 # format class description if cls.__module__ == "__main__": name = cls.__name__ else: name = "%s.%s" % (cls.__module__,cls.__name__) doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" desc = doc and '%s: %s' % (name,doc) or name row = self.REPORT_CLASS_TMPL % dict( style=ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',desc=desc,count=np + nf + ne,Pass=np,fail=nf,error=ne,cid='c%s' % (cid + 1),) rows.append(row) for tid,(n,e) in enumerate(cls_results): self._generate_report_test(rows,cid,tid,n,e) report = self.REPORT_TMPL % dict( test_list=''.join(rows),count=str(result.success_count + result.failure_count + result.error_count),Pass=str(result.success_count),fail=str(result.failure_count),error=str(result.error_count),) return report def _generate_chart(self,result): chart = self.ECHARTS_SCRIPT % dict( Pass=str(result.success_count),) return chart def _generate_report_test(self,rows,e): # e.g. 'pt1.1','ft1.1',etc has_output = bool(o or e) tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid + 1,tid + 1) name = t.id().split('.')[-1] doc = t.shortDescription() or "" desc = doc and ('%s: %s' % (name,doc)) or name tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL script = self.REPORT_TEST_OUTPUT_TMPL % dict( id=tid,output=saxutils.escape(o + e),) row = tmpl % dict( tid=tid,Class=(n == 0 and 'hiddenRow' or 'none'),style=(n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none')),script=script,status=self.STATUS[n],) rows.append(row) if not has_output: return def _generate_ending(self): return self.ENDING_TMPL ############################################################################## # Facilities for running tests from the command line ############################################################################## # Note: Reuse unittest.TestProgram to launch test. In the future we may # build our own launcher to support more specific command line # parameters like test title,CSS,etc. class TestProgram(unittest.TestProgram): """ A variation of the unittest.TestProgram. Please refer to the base class for command line parameters. """ def runTests(self): # Pick HTMLTestRunner as the default test runner. # base class's testRunner parameter is not useful because it means # we have to instantiate HTMLTestRunner before we know self.verbosity. if self.testRunner is None: self.testRunner = HTMLTestRunner(verbosity=self.verbosity) unittest.TestProgram.runTests(self) main = TestProgram ############################################################################## # Executing this module from the command line ############################################################################## if __name__ == "__main__": main(module=None)
九、Python+unittest+requests+HTMLTestRunner完整的介面自動化測試框架搭建_08——呼叫生成測試報告
先別急著建立runAll.py檔案(所有工作做完,最後我們執行runAll.py檔案來執行介面自動化的測試工作並生成測試報告發送報告到相關人郵箱),但是我們在建立此檔案前,還缺少點東東。按我的目錄結構建立caselist.txt檔案,內容如下:
user/test01case #user/test02case #user/test03case #user/test04case #user/test05case #shop/test_shop_list #shop/test_my_shop #shop/test_new_shop
這個檔案的作用是,我們通過這個檔案來控制,執行哪些模組下的哪些unittest用例檔案。如在實際的專案中:user模組下的test01case.py,店鋪shop模組下的我的店鋪my_shop,如果本輪無需執行哪些模組的用例的話,就在前面新增#。我們繼續往下走,還缺少一個傳送郵件的檔案。在common下建立configEmail.py檔案,內容如下:
# import os # import win32com.client as win32 # import datetime # import readConfig # import getpathInfo # # # read_conf = readConfig.ReadConfig() # subject = read_conf.get_email('subject')#從配置檔案中讀取,郵件主題 # app = str(read_conf.get_email('app'))#從配置檔案中讀取,郵件型別 # addressee = read_conf.get_email('addressee')#從配置檔案中讀取,郵件收件人 # cc = read_conf.get_email('cc')#從配置檔案中讀取,郵件抄送人 # mail_path = os.path.join(getpathInfo.get_Path(),'result','report.html')#獲取測試報告路徑 # # class send_email(): # def outlook(self): # olook = win32.Dispatch("%s.Application" % app) # mail = olook.CreateItem(win32.constants.olMailItem) # mail.To = addressee # 收件人 # mail.CC = cc # 抄送 # mail.Subject = str(datetime.datetime.now())[0:19]+'%s' %subject#郵件主題 # mail.Attachments.Add(mail_path,1,"myFile") # content = """ # 執行測試中…… # 測試已完成!! # 生成報告中…… # 報告已生成…… # 報告已郵件傳送!! # """ # mail.Body = content # mail.Send() # # # if __name__ == '__main__':# 運營此檔案來驗證寫的send_email是否正確 # print(subject) # send_email().outlook() # print("send email ok!!!!!!!!!!") # 兩種方式,第一種是用的win32com,因為系統等各方面原因,反饋win32問題較多,建議改成下面的smtplib方式 import os import smtplib import base64 from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart class SendEmail(object): def __init__(self,username,passwd,recv,title,content,file=None,ssl=False,email_host='smtp.163.com',port=25,ssl_port=465): self.username = username # 使用者名稱 self.passwd = passwd # 密碼 self.recv = recv # 收件人,多個要傳list ['[email protected]','[email protected]] self.title = title # 郵件標題 self.content = content # 郵件正文 self.file = file # 附件路徑,如果不在當前目錄下,要寫絕對路徑 self.email_host = email_host # smtp伺服器地址 self.port = port # 普通埠 self.ssl = ssl # 是否安全連結 self.ssl_port = ssl_port # 安全連結埠 def send_email(self): msg = MIMEMultipart() # 傳送內容的物件 if self.file: # 處理附件的 file_name = os.path.split(self.file)[-1] # 只取檔名,不取路徑 try: f = open(self.file,'rb').read() except Exception as e: raise Exception('附件打不開!!!!') else: att = MIMEText(f,"base64","utf-8") att["Content-Type"] = 'application/octet-stream' # base64.b64encode(file_name.encode()).decode() new_file_name = '=?utf-8?b?' + base64.b64encode(file_name.encode()).decode() + '?=' # 這裡是處理檔名為中文名的,必須這麼寫 att["Content-Disposition"] = 'attachment; filename="%s"' % (new_file_name) msg.attach(att) msg.attach(MIMEText(self.content)) # 郵件正文的內容 msg['Subject'] = self.title # 郵件主題 msg['From'] = self.username # 傳送者賬號 msg['To'] = ','.join(self.recv) # 接收者賬號列表 if self.ssl: self.smtp = smtplib.SMTP_SSL(self.email_host,port=self.ssl_port) else: self.smtp = smtplib.SMTP(self.email_host,port=self.port) # 傳送郵件伺服器的物件 self.smtp.login(self.username,self.passwd) try: self.smtp.sendmail(self.username,self.recv,msg.as_string()) pass except Exception as e: print('出錯了。。',e) else: print('傳送成功!') self.smtp.quit() if __name__ == '__main__': m = SendEmail( username='@163.com',passwd='',recv=[''],title='',content='測試傳送郵件',file=r'E:\test_record\v2.3.3\測試截圖\調整樣式.png',ssl=True,) m.send_email()
執行configEmail.py驗證郵件傳送是否正確
郵件已傳送成功,我們進入到郵箱中進行檢視,一切OK~~不過這我要說明一下,我寫的send_email是呼叫的outlook,如果您的電腦本地是使用的其他郵件伺服器的話,這塊的程式碼需要修改為您想使用的郵箱呼叫程式碼
如果遇到傳送的多個收件人,但是隻有第一個收件人可以收到郵件,或者收件人為空可以參考https://www.jb51.net/article/197064.htm
繼續往下走,這下我們該建立我們的runAll.py檔案了
import os import common.HTMLTestRunner as HTMLTestRunner import getpathInfo import unittest import readConfig from common.configEmail import SendEmail from apscheduler.schedulers.blocking import BlockingScheduler import pythoncom # import common.Log send_mail = SendEmail( username='@163.com',) path = getpathInfo.get_Path() report_path = os.path.join(path,'result') on_off = readConfig.ReadConfig().get_email('on_off') # log = common.Log.logger class AllTest:#定義一個類AllTest def __init__(self):#初始化一些引數和資料 global resultPath resultPath = os.path.join(report_path,"report.html")#result/report.html self.caseListFile = os.path.join(path,"caselist.txt")#配置執行哪些測試檔案的配置檔案路徑 self.caseFile = os.path.join(path,"testCase")#真正的測試斷言檔案路徑 self.caseList = [] def set_case_list(self): """ 讀取caselist.txt檔案中的用例名稱,並新增到caselist元素組 :return: """ fb = open(self.caseListFile) for value in fb.readlines(): data = str(value) if data != '' and not data.startswith("#"):# 如果data非空且不以#開頭 self.caseList.append(data.replace("\n",""))#讀取每行資料會將換行轉換為\n,去掉每行資料中的\n fb.close() def set_case_suite(self): """ :return: """ self.set_case_list()#通過set_case_list()拿到caselist元素組 test_suite = unittest.TestSuite() suite_module = [] for case in self.caseList:#從caselist元素組中迴圈取出case case_name = case.split("/")[-1]#通過split函式來將aaa/bbb分割字串,-1取後面,0取前面 print(case_name+".py")#打印出取出來的名稱 #批量載入用例,第一個引數為用例存放路徑,第一個引數為路徑檔名 discover = unittest.defaultTestLoader.discover(self.caseFile,pattern=case_name + '.py',top_level_dir=None) suite_module.append(discover)#將discover存入suite_module元素組 print('suite_module:'+str(suite_module)) if len(suite_module) > 0:#判斷suite_module元素組是否存在元素 for suite in suite_module:#如果存在,迴圈取出元素組內容,命名為suite for test_name in suite:#從discover中取出test_name,使用addTest新增到測試集 test_suite.addTest(test_name) else: print('else:') return None return test_suite#返回測試集 def run(self): """ run test :return: """ try: suit = self.set_case_suite()#呼叫set_case_suite獲取test_suite print('try') print(str(suit)) if suit is not None:#判斷test_suite是否為空 print('if-suit') fp = open(resultPath,'wb')#開啟result/20181108/report.html測試報告檔案,如果不存在就建立 #呼叫HTMLTestRunner runner = HTMLTestRunner.HTMLTestRunner(stream=fp,title='Test Report',description='Test Description') runner.run(suit) else: print("Have no case to test.") except Exception as ex: print(str(ex)) #log.info(str(ex)) finally: print("*********TEST END*********") #log.info("*********TEST END*********") fp.close() #判斷郵件傳送的開關 if on_off == 'on': send_mail.send_email() else: print("郵件傳送開關配置關閉,請開啟開關後可正常自動傳送測試報告") # pythoncom.CoInitialize() # scheduler = BlockingScheduler() # scheduler.add_job(AllTest().run,'cron',day_of_week='1-5',hour=14,minute=59) # scheduler.start() if __name__ == '__main__': AllTest().run()
執行runAll.py,進到郵箱中檢視傳送的測試結果報告,開啟檢視
然後繼續,我們框架到這裡就算基本搭建好了,但是缺少日誌的輸出,在一些關鍵的引數呼叫的地方我們來輸出一些日誌。從而更方便的來維護和查詢問題。
按目錄結構繼續在common下建立Log.py,內容如下:
import os import logging from logging.handlers import TimedRotatingFileHandler import getpathInfo path = getpathInfo.get_Path() log_path = os.path.join(path,'result') # 存放log檔案的路徑 class Logger(object): def __init__(self,logger_name='logs…'): self.logger = logging.getLogger(logger_name) logging.root.setLevel(logging.NOTSET) self.log_file_name = 'logs' # 日誌檔案的名稱 self.backup_count = 5 # 最多存放日誌的數量 # 日誌輸出級別 self.console_output_level = 'WARNING' self.file_output_level = 'DEBUG' # 日誌輸出格式 self.formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') def get_logger(self): """在logger中新增日誌控制代碼並返回,如果logger已有控制代碼,則直接返回""" if not self.logger.handlers: # 避免重複日誌 console_handler = logging.StreamHandler() console_handler.setFormatter(self.formatter) console_handler.setLevel(self.console_output_level) self.logger.addHandler(console_handler) # 每天重新建立一個日誌檔案,最多保留backup_count份 file_handler = TimedRotatingFileHandler(filename=os.path.join(log_path,self.log_file_name),when='D',interval=1,backupCount=self.backup_count,delay=True,encoding='utf-8') file_handler.setFormatter(self.formatter) file_handler.setLevel(self.file_output_level) self.logger.addHandler(file_handler) return self.logger logger = Logger().get_logger()
然後我們在需要我們輸出日誌的地方新增日誌:
我們修改runAll.py檔案,在頂部增加import common.Log,然後增加標紅框的程式碼
讓我們再來執行一下runAll.py檔案,發現在result下多了一個logs檔案,我們開啟看一下有沒有我們列印的日誌
OK,至此我們的介面自動化測試的框架就搭建完了,後續我們可以將此框架進行進一步優化改造,使用我們真實專案的介面,結合持續整合定時任務等,讓這個專案每天定時的來跑啦~~~
2020年9月23追加
一、、最近有太多人反饋,執行通過後report.html檔案中內容為空,這個基本上多數原因是因為用例執行異常報錯,導致沒有成功執行用例,所以沒有生成資料。大家可以執行testCase下的test01Case.py等用例檔案,看是不是執行報錯了。如果執行成功,再去執行runAll試一下
完整的框架原始碼下載
到此這篇關於Python+unittest+requests 介面自動化測試框架搭建教程的文章就介紹到這了,更多相關Python+unittest+requests 介面自動化測試內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!