1. 程式人生 > 實用技巧 >(純技術乾貨)完整的框架搭建過程 實戰 Python+unittest+requests 介面自動化測試

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

一、Python+unittest+requests+HTMLTestRunner 完整的介面自動化測試框架搭建——框架結構簡解

首先配置好開發環境,下載安裝Python並下載安裝pycharm,在pycharm中建立專案功能目錄。如果不會的可以百度Google一下,該內容網上的講解還是比較多比較全的!

大家可以先簡單瞭解下該專案的目錄結構介紹,後面會針對每個檔案有詳細註解和程式碼。

common:

——configDb.py:這個檔案主要編寫資料庫連線池的相關內容,本專案暫未考慮使用資料庫來儲存讀取資料,此檔案可忽略,或者不建立。本人是留著以後如果有相關操作時,方便使用。

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

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

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

——Log.py:呼叫該類的方法,用來列印生成日誌

result:

——logs:生成的日誌檔案

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

testCase:

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

testFile/case:

——userCase.xlsx:對下面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:測試資料庫連線池的檔案,本次專案未用到資料庫,可以忽略

二、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自動化、介面、框架搭建學習資料!技術大牛解惑答疑,同行一起交流

三、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 = [email protected]
cc = [email protected]

在HTTP中,協議http,baseURL,埠,超時時間。

在郵件中on_off是設定的一個開關,=on開啟,傳送郵件,=其他不傳送郵件。subject郵件主題,addressee收件人,cc抄送人。

在我們編寫readConfig.py檔案前,我們先寫一個獲取專案某路徑下某檔案絕對路徑的一個方法。按第一講的目錄結構建立好getpathInfo.py,開啟該檔案

import os
 
def get_Path():
    path = os.path.split(os.path.realpath(__file__))[0]
    return path
 
if __name__ == '__main__':# 執行該檔案,測試下是否OK
    print('測試路徑是否OK,路徑為:', get_Path())

填寫如上程式碼並執行後,檢視輸出結果,打印出了該專案的絕對路徑:  

繼續往下走,同理,按第一講目錄建立好readConfig.py檔案,開啟該檔案

import os
import configparser
import getpathInfo#引入我們自己的寫的獲取路徑的類
 
path = getpathInfo.get_Path()#呼叫例項化,還記得這個類返回的路徑為C:\Users\songlihui\PycharmProjects\dkxinterfaceTest
config_path = os.path.join(path, 'config.ini')#這句話是在path路徑下再加一級,最後變成C:\Users\songlihui\PycharmProjects\dkxinterfaceTest\config.ini
config = configparser.ConfigParser()#呼叫外部的讀取配置檔案的方法
config.read(config_path, encoding='utf-8')
 
class ReadConfig():
 
    def get_http(self, name):
        value = config.get('HTTP', name)
        return value
    def get_email(self, name):
        value = config.get('EMAIL', name)
        return value
    def get_mysql(self, name):#寫好,留以後備用。但是因為我們沒有對資料庫的操作,所以這個可以遮蔽掉
        value = config.get('DATABASE', name)
        return value
 
 
if __name__ == '__main__':#測試一下,我們讀取配置檔案的方法是否可用
    print('HTTP中的baseurl值為:', ReadConfig().get_http('baseurl'))
    print('EMAIL中的開關on_off值為:', ReadConfig().get_email('on_off'))

執行下readConfig.py,檢視資料是否正確 

四、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])

結果為:  

五、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)

執行該檔案,驗證結果正確性:  

六、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?和我們請求的一致  

七、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)

 

八、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)

 

九、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 ['[email protected]','[email protected]]
        self.title = title  # 郵件標題
        self.content = content  # 郵件正文
        self.file = file  # 附件路徑,如果不在當前目錄下,要寫絕對路徑
        self.email_host = email_host  # smtp伺服器地址
        self.port = port  # 普通埠
        self.ssl = ssl  # 是否安全連結
        self.ssl_port = ssl_port  # 安全連結埠
 
    def send_email(self):
        msg = MIMEMultipart()
        # 傳送內容的物件
        if self.file:  # 處理附件的
            file_name = os.path.split(self.file)[-1]  # 只取檔名,不取路徑
            try:
                f = open(self.file, 'rb').read()
            except Exception as e:
                raise Exception('附件打不開!!!!')
            else:
                att = MIMEText(f, "base64", "utf-8")
                att["Content-Type"] = 'application/octet-stream'
                # base64.b64encode(file_name.encode()).decode()
                new_file_name = '=?utf-8?b?' + base64.b64encode(file_name.encode()).decode() + '?='
                # 這裡是處理檔名為中文名的,必須這麼寫
                att["Content-Disposition"] = 'attachment; filename="%s"' % (new_file_name)
                msg.attach(att)
        msg.attach(MIMEText(self.content))  # 郵件正文的內容
        msg['Subject'] = self.title  # 郵件主題
        msg['From'] = self.username  # 傳送者賬號
        msg['To'] = ','.join(self.recv)  # 接收者賬號列表
        if self.ssl:
            self.smtp = smtplib.SMTP_SSL(self.email_host, port=self.ssl_port)
        else:
            self.smtp = smtplib.SMTP(self.email_host, port=self.port)
        # 傳送郵件伺服器的物件
        self.smtp.login(self.username, self.passwd)
        try:
            self.smtp.sendmail(self.username, self.recv, msg.as_string())
            pass
        except Exception as e:
            print('出錯了。。', e)
        else:
            print('傳送成功!')
        self.smtp.quit()
 
 
if __name__ == '__main__':
 
    m = SendEmail(
        username='@163.com',
        passwd='',
        recv=[''],
        title='',
        content='測試傳送郵件',
        file=r'E:\test_record\v2.3.3\測試截圖\調整樣式.png',
        ssl=True,
    )
    m.send_email()

執行configEmail.py驗證郵件傳送是否正確

郵件已傳送成功,我們進入到郵箱中進行檢視,一切OK~~不過這我要說明一下,我寫的send_email是呼叫的outlook,如果您的電腦本地是使用的其他郵件伺服器的話,這塊的程式碼需要修改為您想使用的郵箱呼叫程式碼

如果遇到傳送的多個收件人,但是隻有第一個收件人可以收到郵件,或者收件人為空可以參考

繼續往下走,這下我們該建立我們的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自動化、介面、框架搭建學習資料!技術大牛解惑答疑,同行一起交流