欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Python+unittest+requests 接口自動化測試框架搭建教程

 更新時間:2020年10月09日 10:06:32   作者:songlh1234  
這篇文章主要介紹了Python+unittest+requests 接口自動化測試框架搭建教程,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

一、Python+unittest+requests+HTMLTestRunner 完整的接口自動化測試框架搭建_00——框架結(jié)構(gòu)簡解

 首先配置好開發(fā)環(huán)境,下載安裝Python并下載安裝pycharm,在pycharm中創(chuàng)建項目功能目錄。如果不會的可以百度Google一下,該內(nèi)容網(wǎng)上的講解還是比較多比較全的!

大家可以先簡單了解下該項目的目錄結(jié)構(gòu)介紹,后面會針對每個文件有詳細注解和代碼。

common:

——configDb.py:這個文件主要編寫數(shù)據(jù)庫連接池的相關(guān)內(nèi)容,本項目暫未考慮使用數(shù)據(jù)庫來存儲讀取數(shù)據(jù),此文件可忽略,或者不創(chuàng)建。本人是留著以后如果有相關(guān)操作時,方便使用。

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

——configHttp.py:這個文件主要來通過get、post、put、delete等方法來進行http請求,并拿到請求響應(yīng)。

——HTMLTestRunner.py:主要是生成測試報告相關(guān)

——Log.py:調(diào)用該類的方法,用來打印生成日志

result:

——logs:生成的日志文件

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

testCase:

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

testFile/case:

——userCase.xlsx:對下面test_api.py接口服務(wù)里的接口,設(shè)計了三條簡單的測試用例,如參數(shù)為null,參數(shù)不正確等

caselist.txt:配置將要執(zhí)行testCase目錄下的哪些用例文件,前加#代表不進行執(zhí)行。當(dāng)項目過于龐大,用例足夠多的時候,我們可以通過這個開關(guān),來確定本次執(zhí)行哪些接口的哪些用例。

config.ini:數(shù)據(jù)庫、郵箱、接口等的配置項,用于方便的調(diào)用讀取。

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

geturlParams.py:獲取接口的URL、參數(shù)、method等

readConfig.py:讀取配置文件的方法,并返回文件中內(nèi)容

readExcel.py:讀取Excel的方法

runAll.py:開始執(zhí)行接口自動化,項目工程部署完畢后直接運行該文件即可

test_api.py:自己寫的提供本地測試的接口服務(wù)

test_sql.py:測試數(shù)據(jù)庫連接池的文件,本次項目未用到數(shù)據(jù)庫,可以忽略

二、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建_01——測試接口服務(wù)

首先,我們想搭建一個接口自動化測試框架,前提我們必須要有一個可支持測試的接口服務(wù)。有人可能會說,現(xiàn)在我們的環(huán)境不管測試環(huán)境,還是生產(chǎn)環(huán)境有現(xiàn)成的接口。但是,一般工作環(huán)境中的接口,不太滿足我們框架的各種條件。舉例如,接口a可能是get接口b可能又是post,等等等等。因此我決定自己寫一個簡單的接口!用于我們這個框架的測試!

按第一講的目錄創(chuàng)建好文件,打開test_api.py,寫入如下代碼

import flask
import json
from flask import request

'''
flask: web框架,通過flask提供的裝飾器@server.route()將普通函數(shù)轉(zhuǎn)換為服
'''
# 創(chuàng)建一個服務(wù),把當(dāng)前這個python文件當(dāng)做一個服務(wù)
server = flask.Flask(__name__)
# @server.route()可以將普通函數(shù)轉(zhuǎn)變?yōu)榉?wù) 登錄接口的路徑、請求方式
@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) # 將字典轉(zhuǎn)換字符串
  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回車,驗證我們的接口服務(wù)是否正常~

變更我們的參數(shù),查看不同的響應(yīng)結(jié)果確認接口服務(wù)一切正常

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

在我們第二講中,我們已經(jīng)通過flask這個web框架創(chuàng)建好了我們用于測試的接口服務(wù),因此我們可以把這個接口抽出來一些參數(shù)放到配置文件,然后通過一個讀取配置文件的方法,方便后續(xù)的使用。同樣還有郵件的相關(guān)配置~

按第一講的目錄創(chuàng)建好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中,協(xié)議http,baseURL,端口,超時時間。

在郵件中on_off是設(shè)置的一個開關(guān),=on打開,發(fā)送郵件,=其他不發(fā)送郵件。subject郵件主題,addressee收件人,cc抄送人。

在我們編寫readConfig.py文件前,我們先寫一個獲取項目某路徑下某文件絕對路徑的一個方法。按第一講的目錄結(jié)構(gòu)創(chuàng)建好getpathInfo.py,打開該文件

import os

def get_Path():
 path = os.path.split(os.path.realpath(__file__))[0]
 return path

if __name__ == '__main__':# 執(zhí)行該文件,測試下是否OK
 print('測試路徑是否OK,路徑為:', get_Path())

填寫如上代碼并執(zhí)行后,查看輸出結(jié)果,打印出了該項目的絕對路徑:

繼續(xù)往下走,同理,按第一講目錄創(chuàng)建好readConfig.py文件,打開該文件,以后的章節(jié)不在累贅

import os
import configparser
import getpathInfo#引入我們自己的寫的獲取路徑的類

path = getpathInfo.get_Path()#調(diào)用實例化,還記得這個類返回的路徑為C:\Users\songlihui\PycharmProjects\dkxinterfaceTest
config_path = os.path.join(path, 'config.ini')#這句話是在path路徑下再加一級,最后變成C:\Users\songlihui\PycharmProjects\dkxinterfaceTest\config.ini
config = configparser.ConfigParser()#調(diào)用外部的讀取配置文件的方法
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 value


if __name__ == '__main__':#測試一下,我們讀取配置文件的方法是否可用
 print('HTTP中的baseurl值為:', ReadConfig().get_http('baseurl'))
 print('EMAIL中的開關(guān)on_off值為:', ReadConfig().get_email('on_off'))

執(zhí)行下readConfig.py,查看數(shù)據(jù)是否正確

一切OK

四、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建_03——讀取Excel中的case

配置文件寫好了,接口我們也有了,然后我們來根據(jù)我們的接口設(shè)計我們簡單的幾條用例。首先在前兩講中我們寫了一個我們測試的接口服務(wù),針對這個接口服務(wù)存在三種情況的校驗。正確的用戶名和密碼,賬號密碼錯誤和賬號密碼為空

我們根據(jù)上面的三種情況,將對這個接口的用例寫在一個對應(yīng)的單獨文件中testFile\case\userCase.xlsx ,userCase.xlsx內(nèi)容如下:

緊接著,我們有了用例設(shè)計的Excel了,我們要對這個Excel進行數(shù)據(jù)的讀取操作,繼續(xù)往下,我們創(chuàng)建readExcel.py文件

import os
import getpathInfo# 自己定義的內(nèi)部類,該類返回項目的絕對路徑
#調(diào)用讀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內(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 cls
if __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])

結(jié)果為:

完全正確~

五、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建_04——requests請求

配置文件有了,讀取配置文件有了,用例有了,讀取用例有了,我們的接口服務(wù)有了,我們是不是該寫對某個接口進行http請求了,這時候我們需要使用pip install requests來安裝第三方庫,在common下configHttp.py,configHttp.py的內(nèi)容如下:

import requests
import json


class 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 result


if __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í)行該文件,驗證結(jié)果正確性:

我們發(fā)現(xiàn)和瀏覽器中進行請求該接口,得到的結(jié)果一致,說明沒有問題,一切OK

六、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建_05——參數(shù)動態(tài)化

在上一講中,我們寫了針對我們的接口服務(wù),設(shè)計的三種測試用例,使用寫死的參數(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 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())

通過將配置文件中的進行拼接,拼接后的結(jié)果:http://127.0.0.1:8888/login?和我們請求的一致

七、Python+unittest+requests+HTMLTestRunner完整的接口自動化測試框架搭建_06——unittest斷言

以上的我們都準備好了,剩下的該寫我們的unittest斷言測試case了,在testCase下創(chuàng)建test01case.py文件,文件中內(nèi)容如下:

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()# 調(diào)用我們的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("測試結(jié)束,輸出log完結(jié)\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=轉(zhuǎn)換為{'name':'xxx','pwd':'bbb'}
  info = RunMain().run_main(self.method, url, data1)# 根據(jù)Excel中的method調(diào)用run_main來進行requests請求,并拿到響應(yīng)
  ss = json.loads(info)# 將響應(yīng)轉(zhuǎn)換為字典格式
  if self.case_name == 'login':# 如果case_name是login,說明合法,返回的code應(yīng)該為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

按我的目錄結(jié)構(gòu),在common下創(chuàng)建HTMLTestRunner.py文件,內(nèi)容如下:

# -*- 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添加執(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 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  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,'&amp;');
  s = s.replace(/</g,'&lt;');
  s = s.replace(/>/g,'&gt;');
  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} (vvxyksv9kd%%)"
   },
   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" 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)'>總結(jié)</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>&nbsp;</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'>&nbsp;</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'狀態(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——調(diào)用生成測試報告

先別急著創(chuàng)建runAll.py文件(所有工作做完,最后我們運行runAll.py文件來執(zhí)行接口自動化的測試工作并生成測試報告發(fā)送報告到相關(guān)人郵箱),但是我們在創(chuàng)建此文件前,還缺少點東東。按我的目錄結(jié)構(gòu)創(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 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 # 附件路徑,如果不在當(dāng)前目錄下,要寫絕對路徑
  self.email_host = email_host # smtp服務(wù)器地址
  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ā)送郵件服務(wù)器的對象
  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\測試截圖\調(diào)整樣式.png',
  ssl=True,
 )
 m.send_email()

運行configEmail.py驗證郵件發(fā)送是否正確

郵件已發(fā)送成功,我們進入到郵箱中進行查看,一切OK~~不過這我要說明一下,我寫的send_email是調(diào)用的outlook,如果您的電腦本地是使用的其他郵件服務(wù)器的話,這塊的代碼需要修改為您想使用的郵箱調(diào)用代碼

如果遇到發(fā)送的多個收件人,但是只有第一個收件人可以收到郵件,或者收件人為空可以參考http://www.dbjr.com.cn/article/197064.htm

繼續(xù)往下走,這下我們該創(chuàng)建我們的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='測試發(fā)送郵件',
  file=r'E:\test_record\v2.3.3\測試截圖\調(diào)整樣式.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):#初始化一些參數(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ù)會將換行轉(zhuǎn)換為\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()#調(diào)用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)建
    #調(diào)用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ā)送的開關(guān)
  if on_off == 'on':
   send_mail.send_email()
  else:
   print("郵件發(fā)送開關(guān)配置關(guān)閉,請打開開關(guān)后可正常自動發(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ā)送的測試結(jié)果報告,打開查看

然后繼續(xù),我們框架到這里就算基本搭建好了,但是缺少日志的輸出,在一些關(guān)鍵的參數(shù)調(diào)用的地方我們來輸出一些日志。從而更方便的來維護和查找問題。

按目錄結(jié)構(gòu)繼續(xù)在common下創(chuàng)建Log.py,內(nèi)容如下:

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 # 最多存放日志的數(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: # 避免重復(fù)日志
   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.logger


logger = Logger().get_logger()

然后我們在需要我們輸出日志的地方添加日志:

我們修改runAll.py文件,在頂部增加import common.Log,然后增加標紅框的代碼

讓我們再來運行一下runAll.py文件,發(fā)現(xiàn)在result下多了一個logs文件,我們打開看一下有沒有我們打印的日志

OK,至此我們的接口自動化測試的框架就搭建完了,后續(xù)我們可以將此框架進行進一步優(yōu)化改造,使用我們真實項目的接口,結(jié)合持續(xù)集成定時任務(wù)等,讓這個項目每天定時的來跑啦~~~

2020年9月23追加

一、、最近有太多人反饋,執(zhí)行通過后report.html文件中內(nèi)容為空,這個基本上多數(shù)原因是因為用例執(zhí)行異常報錯,導(dǎo)致沒有成功執(zhí)行用例,所以沒有生成數(shù)據(jù)。大家可以運行testCase下的test01Case.py等用例文件,看是不是運行報錯了。如果運行成功,再去執(zhí)行runAll試一下

完整的框架源碼下載 

到此這篇關(guān)于Python+unittest+requests 接口自動化測試框架搭建教程的文章就介紹到這了,更多相關(guān)Python+unittest+requests 接口自動化測試內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論