您好,登錄后才能下訂單哦!
本篇文章給大家分享的是有關Python+unittest+requests 如何搭建接口自動化測試框架,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
一、Python+unittest+requests+HTMLTestRunner 完整的接口自動化測試框架搭建_00——框架結構簡解
首先配置好開發(fā)環(huán)境,下載安裝Python并下載安裝pycharm,在pycharm中創(chuàng)建項目功能目錄。如果不會的可以百度Google一下,該內(nèi)容網(wǎng)上的講解還是比較多比較全的!
大家可以先簡單了解下該項目的目錄結構介紹,后面會針對每個文件有詳細注解和代碼。
common:
——configDb.py:這個文件主要編寫數(shù)據(jù)庫連接池的相關內(nèi)容,本項目暫未考慮使用數(shù)據(jù)庫來存儲讀取數(shù)據(jù),此文件可忽略,或者不創(chuàng)建。本人是留著以后如果有相關操作時,方便使用。
——configEmail.py:這個文件主要是配置發(fā)送郵件的主題、正文等,將測試報告發(fā)送并抄送到相關人郵箱的邏輯。
——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:對下面test_api.py接口服務里的接口,設計了三條簡單的測試用例,如參數(shù)為null,參數(shù)不正確等
caselist.txt:配置將要執(zhí)行testCase目錄下的哪些用例文件,前加#代表不進行執(zhí)行。當項目過于龐大,用例足夠多的時候,我們可以通過這個開關,來確定本次執(zhí)行哪些接口的哪些用例。
config.ini:數(shù)據(jù)庫、郵箱、接口等的配置項,用于方便的調用讀取。
getpathInfo.py:獲取項目絕對路徑
geturlParams.py:獲取接口的URL、參數(shù)、method等
readConfig.py:讀取配置文件的方法,并返回文件中內(nèi)容
readExcel.py:讀取Excel的方法
runAll.py:開始執(zhí)行接口自動化,項目工程部署完畢后直接運行該文件即可
test_api.py:自己寫的提供本地測試的接口服務
test_sql.py:測試數(shù)據(jù)庫連接池的文件,本次項目未用到數(shù)據(jù)庫,可以忽略
二、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建_01——測試接口服務
首先,我們想搭建一個接口自動化測試框架,前提我們必須要有一個可支持測試的接口服務。有人可能會說,現(xiàn)在我們的環(huán)境不管測試環(huán)境,還是生產(chǎn)環(huán)境有現(xiàn)成的接口。但是,一般工作環(huán)境中的接口,不太滿足我們框架的各種條件。舉例如,接口a可能是get接口b可能又是post,等等等等。因此我決定自己寫一個簡單的接口!用于我們這個框架的測試!
按第一講的目錄創(chuàng)建好文件,打開test_api.py,寫入如下代碼
import flaskimport jsonfrom flask import request'''flask: web框架,通過flask提供的裝飾器@server.route()將普通函數(shù)轉換為服'''# 創(chuàng)建一個服務,把當前這個python文件當做一個服務server = flask.Flask(__name__)# @server.route()可以將普通函數(shù)轉變?yōu)榉?登錄接口的路徑、請求方式@server.route('/login', methods=['get', 'post'])def login(): # 獲取通過url請求傳參的數(shù)據(jù) 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': '參數(shù)不能為空!'} return json.dumps(resu, ensure_ascii=False)if __name__ == '__main__': server.run(debug=True, port=8888, host='127.0.0.1')
執(zhí)行test_api.py,在瀏覽器中輸入http://127.0.0.1:8888/login?name=xiaoming&pwd=11199回車,驗證我們的接口服務是否正常~
變更我們的參數(shù),查看不同的響應結果確認接口服務一切正常
三、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建_02——配置文件讀取
在我們第二講中,我們已經(jīng)通過flask這個web框架創(chuàng)建好了我們用于測試的接口服務,因此我們可以把這個接口抽出來一些參數(shù)放到配置文件,然后通過一個讀取配置文件的方法,方便后續(xù)的使用。同樣還有郵件的相關配置~
按第一講的目錄創(chuàng)建好config.ini文件,打開該文件寫入如下:
# -*- coding: utf-8 -*-[HTTP]scheme = httpbaseurl = 127.0.0.1port = 8888timeout = 10.0[EMAIL]on_off = on;subject = 接口自動化測試報告app = Outlookaddressee = songxiaobao@qq.comcc = zhaobenshan@qq.com
在HTTP中,協(xié)議http,baseURL,端口,超時時間。
在郵件中on_off是設置的一個開關,=on打開,發(fā)送郵件,=其他不發(fā)送郵件。subject郵件主題,addressee收件人,cc抄送人。
在我們編寫readConfig.py文件前,我們先寫一個獲取項目某路徑下某文件絕對路徑的一個方法。按第一講的目錄結構創(chuàng)建好getpathInfo.py,打開該文件
import osdef get_Path(): path = os.path.split(os.path.realpath(__file__))[0] return pathif __name__ == '__main__':# 執(zhí)行該文件,測試下是否OK print('測試路徑是否OK,路徑為:', get_Path())
填寫如上代碼并執(zhí)行后,查看輸出結果,打印出了該項目的絕對路徑:
繼續(xù)往下走,同理,按第一講目錄創(chuàng)建好readConfig.py文件,打開該文件,以后的章節(jié)不在累贅
import osimport configparserimport getpathInfo#引入我們自己的寫的獲取路徑的類path = getpathInfo.get_Path()#調用實例化,還記得這個類返回的路徑為C:\Users\songlihui\PycharmProjects\dkxinterfaceTestconfig_path = os.path.join(path, 'config.ini')#這句話是在path路徑下再加一級,最后變成C:\Users\songlihui\PycharmProjects\dkxinterfaceTest\config.iniconfig = 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):#寫好,留以后備用。但是因為我們沒有對數(shù)據(jù)庫的操作,所以這個可以屏蔽掉 value = config.get('DATABASE', name) return valueif __name__ == '__main__':#測試一下,我們讀取配置文件的方法是否可用 print('HTTP中的baseurl值為:', ReadConfig().get_http('baseurl')) print('EMAIL中的開關on_off值為:', ReadConfig().get_email('on_off'))
執(zhí)行下readConfig.py,查看數(shù)據(jù)是否正確
一切OK
四、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建_03——讀取Excel中的case
配置文件寫好了,接口我們也有了,然后我們來根據(jù)我們的接口設計我們簡單的幾條用例。首先在前兩講中我們寫了一個我們測試的接口服務,針對這個接口服務存在三種情況的校驗。正確的用戶名和密碼,賬號密碼錯誤和賬號密碼為空
我們根據(jù)上面的三種情況,將對這個接口的用例寫在一個對應的單獨文件中testFile\case\userCase.xlsx ,userCase.xlsx內(nèi)容如下:
緊接著,我們有了用例設計的Excel了,我們要對這個Excel進行數(shù)據(jù)的讀取操作,繼續(xù)往下,我們創(chuàng)建readExcel.py文件
import osimport getpathInfo# 自己定義的內(nèi)部類,該類返回項目的絕對路徑#調用讀Excel的第三方庫xlrdfrom 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內(nèi)容行數(shù) nrows = sheet.nrows for i in range(nrows):#根據(jù)行數(shù)做循環(huán) if sheet.row_values(i)[0] != u'case_name':#如果這個Excel的這個sheet的第i行的第一列不等于case_name那么我們把這行的數(shù)據(jù)添加到cls[] cls.append(sheet.row_values(i)) return clsif __name__ == '__main__':#我們執(zhí)行該文件測試一下是否可以正確獲取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的內(nèi)容如下:
import requestsimport jsonclass RunMain(): def send_post(self, url, data): # 定義一個方法,傳入需要的參數(shù)url和data # 參數(shù)必須按照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函數(shù),通過傳過來的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 resultif __name__ == '__main__': # 通過寫死參數(shù),來驗證我們寫的請求是否正確 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)
執(zhí)行該文件,驗證結果正確性:
我們發(fā)現(xiàn)和瀏覽器中進行請求該接口,得到的結果一致,說明沒有問題,一切OK
六、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建_05——參數(shù)動態(tài)化
在上一講中,我們寫了針對我們的接口服務,設計的三種測試用例,使用寫死的參數(shù)(result = RunMain().run_main('post', 'http://127.0.0.1:8888/login', 'name=xiaoming&pwd='))來進行requests請求。本講中我們寫一個類,來用于分別獲取這些參數(shù),來第一講的目錄創(chuàng)建geturlParams.py,geturlParams.py文件中的內(nèi)容如下:
import readConfig as readConfigreadconfig = 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_urlif __name__ == '__main__':# 驗證拼接后的正確性 print(geturlParams().get_Url())
通過將配置文件中的進行拼接,拼接后的結果:http://127.0.0.1:8888/login?和我們請求的一致
七、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建_06——unittest斷言
以上的我們都準備好了,剩下的該寫我們的unittest斷言測試case了,在testCase下創(chuàng)建test01case.py文件,文件中內(nèi)容如下:
import jsonimport unittestfrom common.configHttp import RunMainimport paramunittestimport geturlParamsimport urllib.parse# import pythoncomimport readExcel# pythoncom.CoInitialize()url = geturlParams.geturlParams().get_Url()# 調用我們的geturlParams獲取我們拼接的URLlogin_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)# 根據(jù)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下創(chuàng)建HTMLTestRunner.py文件,內(nèi)容如下:
# -*- coding: utf-8 -*-"""A TestRunner for use with the Python unit testing framework. Itgenerates 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" rel="external nofollow" type="text/css">' # run the test runner.run(my_test_suite)------------------------------------------------------------------------Copyright (c) 2004-2007, Wai Yip TungAll rights reserved.Redistribution and use in source and binary forms, with or withoutmodification, are permitted provided that the following conditions aremet:* 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 "ASIS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITEDTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR APARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNEROR 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, ORPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OFLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDINGNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THISSOFTWARE, 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 HistoryVersion 0.9.1* 用Echarts添加執(zhí)行情況統(tǒng)計圖 (灰藍)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 datetimeimport sysimport ioimport timeimport unittestfrom 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)# ----------------------------------------------------------------------# Templateclass 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="external nofollow" 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')); // 指定圖表的配置項和數(shù)據(jù) var option = { title : { text: '測試執(zhí)行情況', x:'center' }, tooltip : { trigger: 'item', formatter: "{a} <br/> : {c} (spirxlc%%)" }, color: ['#95b75d', 'grey', '#b64645'], legend: { orient: 'vertical', left: 'left', data: ['通過','失敗','錯誤'] }, series : [ { name: '測試執(zhí)行情況', 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)' } } } ] }; // 使用剛指定的配置項和數(shù)據(jù)顯示圖表。 myChart.setOption(option); </script> """ # variables: (Pass, fail, error) # ------------------------------------------------------------------------ # Stylesheet # # alternatively use a <link> for external style sheet, e.g. # <link rel="stylesheet" href="$url" rel="external nofollow" 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 ---------------------------------------------------------------------- */ h2 { 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'> <h2>%(title)s</h2> %(parameters)s </div> <div ><p class='description'>%(description)s</p></div> <div id="chart" ></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>總數(shù)</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)" rel="external nofollow" >詳情</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')" rel="external nofollow" > %(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.TestResultclass _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'狀態(tài)', 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)
九、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建_08——調用生成測試報告
先別急著創(chuàng)建runAll.py文件(所有工作做完,最后我們運行runAll.py文件來執(zhí)行接口自動化的測試工作并生成測試報告發(fā)送報告到相關人郵箱),但是我們在創(chuàng)建此文件前,還缺少點東東。按我的目錄結構創(chuàng)建caselist.txt文件,內(nèi)容如下:
user/test01case#user/test02case#user/test03case#user/test04case#user/test05case#shop/test_shop_list#shop/test_my_shop#shop/test_new_shop
這個文件的作用是,我們通過這個文件來控制,執(zhí)行哪些模塊下的哪些unittest用例文件。如在實際的項目中:user模塊下的test01case.py,店鋪shop模塊下的我的店鋪my_shop,如果本輪無需執(zhí)行哪些模塊的用例的話,就在前面添加#。我們繼續(xù)往下走,還缺少一個發(fā)送郵件的文件。在common下創(chuàng)建configEmail.py文件,內(nèi)容如下:
# 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 = """# 執(zhí)行測試中……# 測試已完成!!# 生成報告中……# 報告已生成……# 報告已郵件發(fā)送??!# """# mail.Body = content# mail.Send()# # # if __name__ == '__main__':# 運營此文件來驗證寫的send_email是否正確# print(subject)# send_email().outlook()# print("send email ok!!!!!!!!!!")# 兩種方式,第一種是用的win32com,因為系統(tǒng)等各方面原因,反饋win32問題較多,建議改成下面的smtplib方式import osimport smtplibimport base64from email.mime.text import MIMETextfrom email.mime.multipart import MIMEMultipartclass 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() # 發(fā)送內(nèi)容的對象 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)) # 郵件正文的內(nèi)容 msg['Subject'] = self.title # 郵件主題 msg['From'] = self.username # 發(fā)送者賬號 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) # 發(fā)送郵件服務器的對象 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('發(fā)送成功!') self.smtp.quit()if __name__ == '__main__': m = SendEmail( username='@163.com', passwd='', recv=[''], title='', content='測試發(fā)送郵件', file=r'E:\test_record\v2.3.3\測試截圖\調整樣式.png', ssl=True, ) m.send_email()
運行configEmail.py驗證郵件發(fā)送是否正確
郵件已發(fā)送成功,我們進入到郵箱中進行查看,一切OK~~不過這我要說明一下,我寫的send_email是調用的outlook,如果您的電腦本地是使用的其他郵件服務器的話,這塊的代碼需要修改為您想使用的郵箱調用代碼
如果遇到發(fā)送的多個收件人,但是只有第一個收件人可以收到郵件,或者收件人為空可以參考http://www.361way.com/smtplib-multiple-addresses/5503.html
繼續(xù)往下走,這下我們該創(chuàng)建我們的runAll.py文件了
import osimport common.HTMLTestRunner as HTMLTestRunnerimport getpathInfoimport unittestimport readConfigfrom common.configEmail import SendEmailfrom apscheduler.schedulers.blocking import BlockingSchedulerimport pythoncom# import common.Logsend_mail = SendEmail( username='@163.com', passwd='', recv=[''], title='', content='測試發(fā)送郵件', 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.loggerclass AllTest:#定義一個類AllTest def __init__(self):#初始化一些參數(shù)和數(shù)據(jù) global resultPath resultPath = os.path.join(report_path, "report.html")#result/report.html self.caseListFile = os.path.join(path, "caselist.txt")#配置執(zhí)行哪些測試文件的配置文件路徑 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", ""))#讀取每行數(shù)據(jù)會將換行轉換為\n,去掉每行數(shù)據(jù)中的\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元素組中循環(huán)取出case case_name = case.split("/")[-1]#通過split函數(shù)來將aaa/bbb分割字符串,-1取后面,0取前面 print(case_name+".py")#打印出取出來的名稱 #批量加載用例,第一個參數(shù)為用例存放路徑,第一個參數(shù)為路徑文件名 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:#如果存在,循環(huán)取出元素組內(nèi)容,命名為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測試報告文件,如果不存在就創(chuàng)建 #調用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() #判斷郵件發(fā)送的開關 if on_off == 'on': send_mail.send_email() else: print("郵件發(fā)送開關配置關閉,請打開開關后可正常自動發(fā)送測試報告")# 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()
執(zhí)行runAll.py,進到郵箱中查看發(fā)送的測試結果報告,打開查看
然后繼續(xù),我們框架到這里就算基本搭建好了,但是缺少日志的輸出,在一些關鍵的參數(shù)調用的地方我們來輸出一些日志。從而更方便的來維護和查找問題。
按目錄結構繼續(xù)在common下創(chuàng)建Log.py,內(nèi)容如下:
import osimport loggingfrom logging.handlers import TimedRotatingFileHandlerimport getpathInfopath = 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 # 最多存放日志的數(shù)量 # 日志輸出級別 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) # 每天重新創(chuàng)建一個日志文件,最多保留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.loggerlogger = Logger().get_logger()
然后我們在需要我們輸出日志的地方添加日志:
我們修改runAll.py文件,在頂部增加import common.Log,然后增加標紅框的代碼
讓我們再來運行一下runAll.py文件,發(fā)現(xiàn)在result下多了一個logs文件,我們打開看一下有沒有我們打印的日志
以上就是Python+unittest+requests 如何搭建接口自動化測試框架,小編相信有部分知識點可能是我們?nèi)粘9ぷ鲿姷交蛴玫降摹OM隳芡ㄟ^這篇文章學到更多知識。更多詳情敬請關注億速云行業(yè)資訊頻道。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。