完整的框架搭建過程 實戰(zhàn) Python+unittest+requests 接口自動化測試

一罢杉、Python+unittest+requests+HTMLTestRunner 完整的接口自動化測試框架搭建——框架結(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完整的接口自動化測試框架搭建——測試接口服務(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ù)是否正常~



希望本文能對你有所幫助,加入我們亿遂,了解更多浓若,642830685,領(lǐng)取最新軟件測試大廠面試資料和Python自動化蛇数、接口挪钓、框架搭建學(xué)習(xí)資料!技術(shù)大牛解惑答疑耳舅,同行一起交流

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

在我們第二講中,我們已經(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完整的接口自動化測試框架搭建——讀取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完整的接口自動化測試框架搭建——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值錯誤D赐铩V苜恕!")
        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完整的接口自動化測試框架搭建——參數(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完整的接口自動化測試框架搭建——unittest斷言

以上的我們都準(zhǔn)備好了矢空,剩下的該寫我們的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+"測試開始前準(zhǔn)備")
 
    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完整的接口自動化測試框架搭建——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">
        // 基于準(zhǔn)備好的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} (hadn2gp%%)"
            },
            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完整的接口自動化測試框架搭建——調(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  # 郵件標(biāo)題
        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('附件打不開!9涨ā2渑!')
            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ā)送的多個收件人鹏溯,但是只有第一個收件人可以收到郵件,或者收件人為空可以參考



繼續(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循榆,然后增加標(biāo)紅框的代碼



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



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

希望本文能對你有所幫助佃延,加入我們,了解更多夷磕,642830685履肃,領(lǐng)取最新軟件測試大廠面試資料和Python自動化、接口坐桩、框架搭建學(xué)習(xí)資料尺棋!技術(shù)大牛解惑答疑,同行一起交流
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末绵跷,一起剝皮案震驚了整個濱河市膘螟,隨后出現(xiàn)的幾起案子成福,更是在濱河造成了極大的恐慌,老刑警劉巖荆残,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件闷叉,死亡現(xiàn)場離奇詭異,居然都是意外死亡脊阴,警方通過查閱死者的電腦和手機握侧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嘿期,“玉大人品擎,你說我怎么就攤上這事”感欤” “怎么了萄传?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蜜猾。 經(jīng)常有香客問我秀菱,道長,這世上最難降的妖魔是什么蹭睡? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任衍菱,我火速辦了婚禮,結(jié)果婚禮上肩豁,老公的妹妹穿的比我還像新娘脊串。我一直安慰自己,他們只是感情好清钥,可當(dāng)我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布琼锋。 她就那樣靜靜地躺著,像睡著了一般祟昭。 火紅的嫁衣襯著肌膚如雪缕坎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天篡悟,我揣著相機與錄音谜叹,去河邊找鬼。 笑死恰力,一個胖子當(dāng)著我的面吹牛叉谜,可吹牛的內(nèi)容都是我干的旗吁。 我是一名探鬼主播踩萎,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼很钓!你這毒婦竟也來了香府?” 一聲冷哼從身側(cè)響起董栽,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎企孩,沒想到半個月后锭碳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡勿璃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年擒抛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片补疑。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡歧沪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出莲组,到底是詐尸還是另有隱情诊胞,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布锹杈,位于F島的核電站撵孤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏竭望。R本人自食惡果不足惜邪码,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望咬清。 院中可真熱鬧霞扬,春花似錦、人聲如沸枫振。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽粪滤。三九已至斧拍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間杖小,已是汗流浹背肆汹。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留予权,地道東北人昂勉。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像扫腺,于是被迫代替她去往敵國和親岗照。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,092評論 2 355