(純技術乾貨)完整的框架搭建過程 實戰 Python+unittest+requests 接口自動化測試

1、Python+unittest+requests+HTMLTestRunner 完整的接口自動化測試框架搭建——框架結構簡解

首先配置好開發環境,下載安裝Python並下載安裝pycharm,在pycharm中建立項目功能目錄。若是不會的能夠百度Google一下,該內容網上的講解仍是比較多比較全的!javascript

 

 

你們能夠先簡單瞭解下該項目的目錄結構介紹,後面會針對每一個文件有詳細註解和代碼。css

common:html

——configDb.py:這個文件主要編寫數據庫鏈接池的相關內容,本項目暫未考慮使用數據庫來存儲讀取數據,此文件可忽略,或者不建立。本人是留着之後若是有相關操做時,方便使用。java

——configEmail.py:這個文件主要是配置發送郵件的主題、正文等,將測試報告發送並抄送到相關人郵箱的邏輯。python

——configHttp.py:這個文件主要來經過get、post、put、delete等方法來進行http請求,並拿到請求響應。mysql

——HTMLTestRunner.py:主要是生成測試報告相關web

——Log.py:調用該類的方法,用來打印生成日誌面試

result:sql

——logs:生成的日誌文件數據庫

——report.html:生成的測試報告

testCase:

——test01case.py:讀取userCase.xlsx中的用例,使用unittest來進行斷言校驗

testFile/case:

——userCase.xlsx:對下面test_api.py接口服務裏的接口,設計了三條簡單的測試用例,如參數爲null,參數不正確等

caselist.txt:配置將要執行testCase目錄下的哪些用例文件,前加#表明不進行執行。當項目過於龐大,用例足夠多的時候,咱們能夠經過這個開關,來肯定本次執行哪些接口的哪些用例。

config.ini:數據庫、郵箱、接口等的配置項,用於方便的調用讀取。

getpathInfo.py:獲取項目絕對路徑

geturlParams.py:獲取接口的URL、參數、method等

readConfig.py:讀取配置文件的方法,並返回文件中內容

readExcel.py:讀取Excel的方法

runAll.py:開始執行接口自動化,項目工程部署完畢後直接運行該文件便可

test_api.py:本身寫的提供本地測試的接口服務

test_sql.py:測試數據庫鏈接池的文件,本次項目未用到數據庫,能夠忽略

 

2、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建——測試接口服務

首先,咱們想搭建一個接口自動化測試框架,前提咱們必需要有一個可支持測試的接口服務。有人可能會說,如今咱們的環境無論測試環境,仍是生產環境有現成的接口。可是,通常工做環境中的接口,不太知足咱們框架的各類條件。舉例如,接口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回車,驗證咱們的接口服務是否正常~  

 

 

 

 

 

 

 

但願本文能對你有所幫助,加入咱們,瞭解更多,642830685,領取最新軟件測試大廠面試資料和Python自動化、接口、框架搭建學習資料!技術大牛解惑答疑,同行一塊兒交流

3、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建——配置文件讀取

在咱們第二講中,咱們已經經過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 = songxiaobao@qq.com cc = zhaobenshan@qq.com 

在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,查看數據是否正確 

 

 

4、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建——讀取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]) 

結果爲:  

 

 

5、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建——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, url, data): result = requests.get(url=url, params=data).json() res = json.dumps(result, ensure_ascii=False, sort_keys=True, 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', 'http://127.0.0.1:8888/login', 'name=xiaoming&pwd=111') print(result1) print(result2) 

執行該文件,驗證結果正確性:  

 

 

6、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建——參數動態化

在上一講中,咱們寫了針對咱們的接口服務,設計的三種測試用例,使用寫死的參數(result = RunMain().run_main('post', 'http://127.0.0.1:8888/login', 'name=xiaoming&pwd='))來進行requests請求。本講中咱們寫一個類,來用於分別獲取這些參數,來第一講的目錄建立geturlParams.pygeturlParams.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?和咱們請求的一致  

 

 

7、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建——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, url, 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) 

 

8、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建——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, 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, fail, 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, count, Pass, fail, error, cid) REPORT_TEST_WITH_OUTPUT_TMPL = r""" <tr id='%(tid)s' class='%(Class)s'> <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> <td colspan='5' align='center'> <!--css div popup start--> <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" > %(status)s</a> <div id='div_%(tid)s' class="popup_window"> <pre>%(script)s</pre> </div> <!--css div popup end--> </td> </tr> """ # variables: (tid, Class, style, desc, status) REPORT_TEST_NO_OUTPUT_TMPL = r""" <tr id='%(tid)s' class='%(Class)s'> <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> <td colspan='5' align='center'>%(status)s</td> </tr> """ # variables: (tid, Class, style, desc, status) REPORT_TEST_OUTPUT_TMPL = r"""%(id)s: %(output)s""" # variables: (id, output) # ------------------------------------------------------------------------ # ENDING # ENDING_TMPL = """<div id='ending'> </div>""" # -------------------- The end of the Template class ------------------- TestResult = unittest.TestResult class _TestResult(TestResult): # note: _TestResult is a pure representation of results. # It lacks the output and reporting ability compares to unittest._TextTestResult. def __init__(self, verbosity=1): TestResult.__init__(self) self.stdout0 = None self.stderr0 = None self.success_count = 0 self.failure_count = 0 self.error_count = 0 self.verbosity = verbosity # result is a list of result in 4 tuple # ( # result code (0: success; 1: fail; 2: error), # TestCase object, # Test output (byte string), # stack trace, # ) self.result = [] 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, test, err): self.error_count += 1 TestResult.addError(self, test, err) _, _exc_str = self.errors[-1] output = self.complete_output() self.result.append((2, test, output, _exc_str)) if self.verbosity > 1: sys.stderr.write('E ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('E') def addFailure(self, test, err): self.failure_count += 1 TestResult.addFailure(self, test, err) _, _exc_str = self.failures[-1] output = self.complete_output() self.result.append((1, test, output, _exc_str)) if self.verbosity > 1: sys.stderr.write('F ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('F') def addSubTest(self, test, 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, test, output + '\nSubTestCase Failed:\n' + str(subtest), self._exc_info_to_string(err, 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, self._exc_info_to_string(err, subtest))) output = self.complete_output() self.result.append( (2, test, output + '\nSubTestCase Error:\n' + str(subtest), self._exc_info_to_string(err, 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, test, 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, t, o, e)) r = [(cls, rmap[cls]) for cls in classes] return r def getReportAttributes(self, result): """ Return report attributes as a list of (name, value). Override this to add custom attributes. """ startTime = str(self.startTime)[:19] duration = str(self.stopTime - self.startTime) status = [] if result.success_count: status.append(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, test, result): report_attrs = self.getReportAttributes(result) generator = 'HTMLTestRunner %s' % __version__ stylesheet = self._generate_stylesheet() heading = self._generate_heading(report_attrs) report = self._generate_report(result) ending = self._generate_ending() 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, t, o, e in cls_results: if n == 0: np += 1 elif n == 1: nf += 1 else: ne += 1 # format class description if cls.__module__ == "__main__": name = cls.__name__ else: name = "%s.%s" % (cls.__module__, cls.__name__) doc = cls.__doc__ and cls.__doc__.split("\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, t, o, e) in enumerate(cls_results): self._generate_report_test(rows, cid, tid, n, t, o, e) report = self.REPORT_TMPL % dict( test_list=''.join(rows), count=str(result.success_count + result.failure_count + result.error_count), Pass=str(result.success_count), fail=str(result.failure_count), error=str(result.error_count), ) return report def _generate_chart(self, result): chart = self.ECHARTS_SCRIPT % dict( Pass=str(result.success_count), fail=str(result.failure_count), error=str(result.error_count), ) return chart def _generate_report_test(self, rows, cid, tid, n, t, o, e): # e.g. 'pt1.1', 'ft1.1', etc has_output = bool(o or e) tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid + 1, tid + 1) name = t.id().split('.')[-1] doc = t.shortDescription() or "" desc = doc and ('%s: %s' % (name, doc)) or name tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 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')), desc=desc, script=script, status=self.STATUS[n], ) rows.append(row) if not has_output: return def _generate_ending(self): return self.ENDING_TMPL ############################################################################## # Facilities for running tests from the command line ############################################################################## # Note: Reuse unittest.TestProgram to launch test. In the future we may # build our own launcher to support more specific command line # parameters like test title, CSS, etc. class TestProgram(unittest.TestProgram): """ A variation of the unittest.TestProgram. Please refer to the base class for command line parameters. """ def runTests(self): # Pick HTMLTestRunner as the default test runner. # base class's testRunner parameter is not useful because it means # we have to instantiate HTMLTestRunner before we know self.verbosity. if self.testRunner is None: self.testRunner = HTMLTestRunner(verbosity=self.verbosity) unittest.TestProgram.runTests(self) main = TestProgram ############################################################################## # Executing this module from the command line ############################################################################## if __name__ == "__main__": main(module=None) 

 

9、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建——調用生成測試報告

先別急着建立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, 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 ['a@qq.com','b@qq.com] 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,若是您的電腦本地是使用的其餘郵件服務器的話,這塊的代碼須要修改成您想使用的郵箱調用代碼

若是遇到發送的多個收件人,可是隻有第一個收件人能夠收到郵件,或者收件人爲空能夠參考

 

 

 繼續往下走,這下咱們該建立咱們的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', passwd='', recv=[''], title='', content='測試發送郵件', file=r'E:\test_record\v2.3.3\測試截圖\調整樣式.png', ssl=True, ) 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,至此咱們的接口自動化測試的框架就搭建完了,後續咱們能夠將此框架進行進一步優化改造,使用咱們真實項目的接口,結合持續集成定時任務等,讓這個項目天天定時的來跑啦~~~

但願本文能對你有所幫助,加入咱們,瞭解更多,642830685,領取最新軟件測試大廠面試資料和Python自動化、接口、框架搭建學習資料!技術大牛解惑答疑,同行一塊兒交流
相關文章
相關標籤/搜索