一個簡單的介面測試框架 demo

Charseki發表於2020-09-06

Python 介面自動化測試框架

基於 Requests+Unittest+HTMLTestRunner,用 Excel 管理測試用例.

正文

有時候也會問自己為什麼要重複造輪子,開源框架一搜一堆。後來想想,可能我在乎的不是目的地,而是沿途的風景。

【框架流程圖】

【Common 部分】
常見的介面都是走 http 協議,對 requests 庫進行 post/get 請求方法的封裝。

# -*- coding: utf-8 -*-
"""
@File:Request.py
@E-mail:364942727@qq.com
@Time:2020/9/5 8:29 下午
@Author:Nobita
@Version:1.0
@Desciption:Request請求封裝模組
"""

import requests
from Common.Log import logger


class RunMain():
    def __init__(self):
        self.logger = logger

    def send_post(self, url, headers, data):  # 定義一個方法,傳入需要的引數url、headers和data
        # 引數必須按照url、headers、data順序傳入
        headers = headers
        result_data = requests.post(url=url, headers=headers, data=data).json()  # 因為這裡要封裝post方法,所以這裡的url和data值不能寫死
        result_json = requests.post(url=url, headers=headers, json=data).json()  # 介面需要json引數提交資料,用這種請求方法
        # res = json.dumps(Log, ensure_ascii=False, sort_keys=True, indent=2)  # 格式化輸出
        res = result_data
        return res

    def send_get(self, url, headers, data):
        headers = headers
        result_data = requests.get(url=url, headers=headers, data=data).json()
        result_json = requests.post(url=url, headers=headers, json=data).json()  # 介面需要json引數提交資料,用這種請求方法
        # res = json.dumps(Log, ensure_ascii=False, sort_keys=True, indent=2)  # 格式化輸出
        res = result_data
        return res

    def run_main(self, method, url=None, headers=None, data=None):  # 定義一個run_main函式,透過傳過來的method來進行不同的get或post請求
        result = None
        if method == 'post':
            result = self.send_post(url, headers, data)
            self.logger.info(str(result))
        elif method == 'get':
            result = self.send_get(url, headers, data)
            self.logger.info(str(result))
        else:
            print("method值錯誤!!!")
            self.logger.info("method值錯誤!!!")
        return result


if __name__ == '__main__':  # 透過寫死引數,來驗證我們寫的請求是否正確
    pass
    # method_post = 'post'
    # url_post = 'http://127.0.0.1:5000/login'
    # data_post = {
    #     "username": "admin",
    #     "password": "a123456"
    # }
    # result_post = RunMain().run_main(method=method_post, url=url_post, data=data_post)
    # print(result_post)

對傳送郵件的 SMTP 模組進行封裝。

# -*- coding: utf-8 -*-
"""
@File:SendEmail.py
@E-mail:364942727@qq.com
@Time:2020/9/5 7:58 下午
@Author:Nobita
@Version:1.0
@Desciption:封裝SMTP郵件功能模組
"""

import os
from Config import readConfig
import getpathInfo
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header
from Common.Log import logger


class SendEmail(object):
    def __init__(self):
        # 讀取郵件配置資訊,初始化引數
        read_conf = readConfig.ReadConfig()
        self.email_service = read_conf.get_email('EMAIL_SERVICE')  # 從配置檔案中讀取,郵件伺服器型別
        self.email_port = read_conf.get_email('EMAIL_PORT')  # 從配置檔案中讀取,郵件伺服器埠
        self.sender_address = read_conf.get_email('SENDER_ADDRESS')  # 從配置檔案中讀取,發件人郵箱地址
        self.sender_password = read_conf.get_email('SENDER_PASSWORD')  # 從配置檔案中讀取,發件人郵箱授權碼
        self.receiver_address = read_conf.get_email('RECEIVER_ADDRESS')  # 從配置檔案中讀取,收件人郵箱地址
        self.file_path = os.path.join(getpathInfo.get_Path(), 'Report', 'report.html')  # 獲取測試報告路徑
        # 日誌輸出
        self.logger = logger

    def send_email(self):
        # 第三方 SMTP 服務
        message = MIMEMultipart()
        # 建立附件的例項
        message['From'] = Header("測試組", 'utf-8')
        message['To'] = Header(''.join(self.receiver_address), 'utf-8')
        subject = '介面測試郵件'
        message['Subject'] = Header(subject, 'utf-8')
        # 郵件正文內容
        part = MIMEText('Dear all:\n       附件為介面自動化測試報告,此為自動傳送郵件,請勿回覆,謝謝!', 'plain', 'utf-8')
        message.attach(part)
        # 傳送附件
        att1 = MIMEText(open(file=self.file_path, mode='r').read(), 'base64', 'utf-8')
        att1["Content-Type"] = 'application/octet-stream'
        att1.add_header('Content-Disposition', 'attachment', filename=('utf-8', '', '介面測試報告.html'))
        message.attach(att1)

        try:

            service = smtplib.SMTP_SSL(self.email_service)
            # service.set_debuglevel(True)  # debug開啟或關閉
            service.connect(self.email_service, self.email_port)
            service.login(self.sender_address, self.sender_password)
            service.sendmail(self.sender_address, self.receiver_address, message.as_string())
            print('郵件傳送成功')
            service.close()
            self.logger.info("{'郵件傳送成功'}")

        except smtplib.SMTPException:
            print("報錯,郵件傳送失敗")
            self.logger.info("{'報錯,郵件傳送失敗'}")


if __name__ == '__main__':
    # SendEmail().send_email()  # 測試郵件功能模組
    pass

常見 assert 斷言模組的封裝。

# -*- coding: utf-8 -*-
"""
@File:Assert.py
@E-mail:364942727@qq.com
@Time:2020/9/5 23:03 下午
@Author:Nobita
@Version:1.0
@Desciption:Assert斷言封裝模組
"""

from Common.Log import logger
import json


class Assertions:
    def __init__(self):
        self.log = logger

    def assert_code(self, code, expected_code):
        """
        驗證response狀態碼
        :param code:
        :param expected_code:
        :return:
        """
        try:
            assert code == expected_code
            return True
        except:
            self.log.info("statusCode error, expected_code is %s, statusCode is %s " % (expected_code, code))

            raise

    def assert_body(self, body, body_msg, expected_msg):
        """
        驗證response body中任意屬性的值
        :param body:
        :param body_msg:
        :param expected_msg:
        :return:
        """
        try:
            msg = body[body_msg]
            assert msg == expected_msg
            return True

        except:
            self.log.info(
                "Response body msg != expected_msg, expected_msg is %s, body_msg is %s" % (expected_msg, body_msg))

            raise

    def assert_in_text(self, body, expected_msg):
        """
        驗證response body中是否包含預期字串
        :param body:
        :param expected_msg:
        :return:
        """
        try:
            text = json.dumps(body, ensure_ascii=False)
            # print(text)
            assert expected_msg in text
            return True

        except:
            self.log.info("Response body Does not contain expected_msg, expected_msg is %s" % expected_msg)

            raise

    def assert_text(self, body, expected_msg):
        """
        驗證response body中是否等於預期字串
        :param body:
        :param expected_msg:
        :return:
        """
        try:
            assert body == expected_msg
            return True

        except:
            self.log.info("Response body != expected_msg, expected_msg is %s, body is %s" % (expected_msg, body))

            raise

    def assert_time(self, time, expected_time):
        """
        驗證response body響應時間小於預期最大響應時間,單位:毫秒
        :param body:
        :param expected_time:
        :return:
        """
        try:
            assert time < expected_time
            return True

        except:
            self.log.info("Response time > expected_time, expected_time is %s, time is %s" % (expected_time, time))

            raise


if __name__ == '__main__':
    # info_body = {'code': 102001, 'message': 'login success'}
    # Assert = Assertions()
    # expect_code = 10200
    # Assert.assert_code(info_body['code'], expect_code)
    pass

對 Log 日誌模組的封裝。

# -*- coding: utf-8 -*-
"""
@File:Log.py
@E-mail:364942727@qq.com
@Time:2020/9/4 8:58 下午
@Author:Nobita
@Version:1.0
@Desciption:Log日誌模組
"""

import os
import logging
from logging.handlers import TimedRotatingFileHandler
import getpathInfo


class Logger(object):
    def __init__(self, logger_name='logs…'):
        global log_path
        path = getpathInfo.get_Path()
        log_path = os.path.join(path, 'Log')  # 存放log檔案的路徑
        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()

if __name__ == "__main__":
    pass

對各種常見加密方法的封裝。

# -*- coding: utf-8 -*-
"""
@File:Hash.py
@E-mail:364942727@qq.com
@Time:2020/9/6 15:55 下午
@Author:Nobita
@Version:1.0
@Desciption:封裝各種常用的加密方法
"""

from hashlib import sha1
from hashlib import md5
from Crypto.Hash import SHA256
from Crypto.Cipher import AES
from Crypto.Cipher import DES
import binascii


class MyHash(object):

    def my_md5(self, msg):
        """
        md5 演算法加密
        :param msg: 需加密的字串
        :return: 加密後的字元
        """
        hl = md5()
        hl.update(msg.encode('utf-8'))
        return hl.hexdigest()

    def my_sha1(self, msg):
        """
        sha1 演算法加密
        :param msg: 需加密的字串
        :return: 加密後的字元
        """
        sh = sha1()
        sh.update(msg.encode('utf-8'))
        return sh.hexdigest()

    def my_sha256(self, msg):
        """
        sha256 演算法加密
        :param msg: 需加密的字串
        :return: 加密後的字元
        """
        sh = SHA256.new()
        sh.update(msg.encode('utf-8'))
        return sh.hexdigest()

    def my_des(self, msg, key):
        """
        DES 演算法加密
        :param msg: 需加密的字串,長度必須為8的倍數,不足新增'='
        :param key: 8個字元
        :return: 加密後的字元
        """
        de = DES.new(key, DES.MODE_ECB)
        mss = msg + (8 - (len(msg) % 8)) * '='
        text = de.encrypt(mss.encode())
        return binascii.b2a_hex(text).decode()

    def my_aes_encrypt(self, msg, key, vi):
        """
        AES 演算法的加密
        :param msg: 需加密的字串
        :param key: 必須為16,24,32位
        :param vi: 必須為16位
        :return: 加密後的字元
        """
        obj = AES.new(key, AES.MODE_CBC, vi)
        txt = obj.encrypt(msg.encode())
        return binascii.b2a_hex(txt).decode()

    def my_aes_decrypt(self, msg, key, vi):
        """
        AES 演算法的解密
        :param msg: 需解密的字串
        :param key: 必須為16,24,32位
        :param vi: 必須為16位
        :return: 加密後的字元
        """
        msg = binascii.a2b_hex(msg)
        obj = AES.new(key, AES.MODE_CBC, vi)
        return obj.decrypt(msg).decode()


if __name__ == "__main__":
    res = MyHash().my_md5('hello world')
    print(res)

獲取配置檔案中拼接後的 base_url

# -*- coding: utf-8 -*-
"""
@File:geturlParams.py
@E-mail:364942727@qq.com
@Time:2020/9/3 9:28 下午
@Author:Nobita
@Version:1.0
@Desciption:獲取配置檔案中拼接後的URL
"""

from Config import readConfig as readConfig


class geturlParams():  # 定義一個方法,將從配置檔案中讀取的進行拼接
    def __init__(self):
        self.readconfig = readConfig.ReadConfig()

    def get_Url(self):
        new_url = self.readconfig.get_http('scheme') + '://' + self.readconfig.get_http(
            'baseurl') + ':' + self.readconfig.get_http(
            'port')
        # logger.info('new_url'+new_url)
        return new_url


if __name__ == '__main__':  # 驗證拼接後的正確性
    print(geturlParams().get_Url())
    # pass

對讀取 Excel 檔案方法的封裝。

# -*- coding: utf-8 -*-
"""
@File:readExcel.py
@E-mail:364942727@qq.com
@Time:2020/9/3 16:58 上午
@Author:Nobita
@Version:1.0
@Desciption:
"""

import os
import getpathInfo
from xlrd import open_workbook  # 呼叫讀Excel的第三方庫xlrd


class readExcel():
    def __init__(self):
        self.path = getpathInfo.get_Path()  # 拿到該專案所在的絕對路徑

    def get_xls(self, xls_name, sheet_name):  # xls_name填寫用例的Excel名稱 sheet_name該Excel的sheet名稱
        cls = []
        # 獲取用例檔案路徑
        xlsPath = os.path.join(self.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('learning-API-test_Case.xlsx', 'login'))  # 遍歷每一行資料
    print(readExcel().get_xls('learning-API-test_Case.xlsx', 'login')[0][1])  # 登入介面url
    print(readExcel().get_xls('learning-API-test_Case.xlsx', 'login')[1][4])  # 請求method
    # pass

對生成 html 介面自動化報告方法的封裝

#coding=utf-8
"""
A TestRunner for use with the Python unit testing framework. It
generates a HTML report to show the Log at a glance.

The simplest way to use this is to invoke its main method. E.g.

    import unittest
    import HTMLTestReportCN

    ... define your tests ...

    if __name__ == '__main__':
        HTMLTestReportCN.main()


For more customization options, instantiates a HTMLTestReportCN object.
HTMLTestReportCN is a counterpart to unittest's TextTestRunner. E.g.

    # output to a file
    fp = file('my_report.html', 'wb')
    runner = HTMLTestReportCN.HTMLTestReportCN(
                stream=fp,
                title='My unit test',
                description='This demonstrates the report output by HTMLTestReportCN.'
                )

    # 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
Copyright (c) 2017, Findyou
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,  Findyou"
__version__ = "0.8.3"


"""
Change History
Version 0.8.3 -Findyou 20171206
* BUG fixed :錯誤的測試用例沒有統計與顯示
* BUG fixed :當PASS的測試用例有print內容時,透過按鈕顯示為紅色
* 表格背景顏色根據用例結果顯示顏色,優先順序: 錯誤(黃色)>失敗(紅色)>透過(綠色)
* 合併文為HTMLTestRunner*N.py 同時支援python2,python3

Version 0.8.2.2 -Findyou
* HTMLTestRunnerEN.py 支援 python3.x
* HTMLTestRunnerEN.py 支援 python2.x

Version 0.8.2.1 -Findyou
* 支援中文,漢化
* 調整樣式,美化(需要連入網路,使用的百度的Bootstrap.js)
* 增加 透過分類顯示、測試人員、透過率的展示
* 最佳化“詳細”與“收起”狀態的變換
* 增加返回頂部的錨點

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
try:
    from StringIO import StringIO
except ImportError:
    from io import StringIO
import sys
import time
import unittest
from xml.sax import saxutils

try:
    reload(sys)
    sys.setdefaultencoding('utf-8')
except NameError:
    pass

# ------------------------------------------------------------------------
# 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: '透過',
    1: '失敗',
    2: '錯誤',
    }

    DEFAULT_TITLE = '測試報告'
    DEFAULT_DESCRIPTION = ''
    DEFAULT_TESTER='QA'

    # ------------------------------------------------------------------------
    # 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://libs.baidu.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    <script src="http://libs.baidu.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
    %(stylesheet)s
</head>
<body >
%(heading)s
%(report)s
%(ending)s
<script language="javascript" type="text/javascript">
output_list = Array();
// 修改按鈕顏色顯示錯誤問題 --Findyou v0.8.2.3

$("button[id^='btn_pt']").addClass("btn btn-success");
$("button[id^='btn_ft']").addClass("btn btn-danger");
$("button[id^='btn_et']").addClass("btn btn-warning");

/*level
增加分類並調整,增加error按鈕事件 --Findyou v0.8.2.3
0:Pass    //pt none, ft hiddenRow, et hiddenRow
1:Failed  //pt hiddenRow, ft none, et hiddenRow
2:Error    //pt hiddenRow, ft hiddenRow, et none
3:All     //pt none, ft none, et none
4:Summary //all hiddenRow
*/

//add Error button event --Findyou v0.8.2.3
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 == 0 || level == 2 || level == 4 ) {
                tr.className = 'hiddenRow';
            }
            else {
                tr.className = '';
            }
        }
        if (id.substr(0,2) == 'pt') {
            if (level == 1 || level == 2 || level == 4) {
                tr.className = 'hiddenRow';
            }
            else {
                tr.className = '';
            }
        }
        if (id.substr(0,2) == 'et') {
            if (level == 0 || level == 1 || level == 4) {
                tr.className = 'hiddenRow';
            }
            else {
                tr.className = '';
            }
        }
    }

    //加入【詳細】切換文字變化 --Findyou
    detail_class=document.getElementsByClassName('detail');
    //console.log(detail_class.length)
    if (level == 3) {
        for (var i = 0; i < detail_class.length; i++){
            detail_class[i].innerHTML="收起"
        }
    }
    else{
            for (var i = 0; i < detail_class.length; i++){
            detail_class[i].innerHTML="詳細"
        }
    }
}

//add Error button event --Findyou v0.8.2.3
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);
        }
        if (!tr) {
            tid = 'e' + 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];
        //修改點選無法收起的BUG,加入【詳細】切換文字變化 --Findyou
        if (toHide) {
            document.getElementById(tid).className = 'hiddenRow';
            document.getElementById(cid).innerText = "詳細"
        }
        else {
            document.getElementById(tid).className = '';
            document.getElementById(cid).innerText = "收起"
        }
    }
}

function html_escape(s) {
    s = s.replace(/&/g,'&');
    s = s.replace(/</g,'<');
    s = s.replace(/>/g,'>');
    return s;
}
</script>
</body>
</html>
"""
    # variables: (title, generator, stylesheet, heading, report, ending)


    # ------------------------------------------------------------------------
    # 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,Tahoma,arial,helvetica,sans-serif;padding: 20px; font-size: 100%; }
table       { font-size: 100%; }

/* -- heading ---------------------------------------------------------------------- */
.heading {
    margin-top: 0ex;
    margin-bottom: 1ex;
}

.heading .description {
    margin-top: 4ex;
    margin-bottom: 6ex;
}

/* -- report ------------------------------------------------------------------------ */
#total_row  { font-weight: bold; }
.passCase   { color: #5cb85c; }
.failCase   { color: #d9534f; font-weight: bold; }
.errorCase  { color: #f0ad4e; font-weight: bold; }
.hiddenRow  { display: none; }
.testcase   { margin-left: 2em; }
</style>
"""

    # ------------------------------------------------------------------------
    # Heading
    #

    HEADING_TMPL = """<div class='heading'>
<h1 style="font-family: Microsoft YaHei">%(title)s</h1>
%(parameters)s
<p class='description'>%(description)s</p>
</div>

""" # variables: (title, parameters, description)

    HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s : </strong> %(value)s</p>
""" # variables: (name, value)



    # ------------------------------------------------------------------------
    # Report
    #
    # 漢化,加美化效果 --Findyou
    REPORT_TMPL = """
<p id='show_detail_line'>
<a class="btn btn-primary" href='javascript:showCase(4)'>概要{ %(passrate)s }</a>
<a class="btn btn-success" href='javascript:showCase(0)'>透過{ %(Pass)s }</a>
<a class="btn btn-danger" href='javascript:showCase(1)'>失敗{ %(fail)s }</a>
<a class="btn btn-warning" href='javascript:showCase(2)'>錯誤{ %(error)s }</a>
<a class="btn btn-info" href='javascript:showCase(3)'>所有{ %(count)s }</a>
</p>
<table id='result_table' class="table table-condensed table-bordered table-hover">
<colgroup>
<col align='left' />
<col align='right' />
<col align='right' />
<col align='right' />
<col align='right' />
<col align='right' />
</colgroup>
<tr id='header_row' class="text-center active" style="font-weight: bold;font-size: 14px;">
    <td>用例集/測試用例</td>
    <td>總計</td>
    <td>透過</td>
    <td>失敗</td>
    <td>錯誤</td>
    <td>詳細</td>
</tr>
%(test_list)s
<tr id='total_row' class="text-center info">
    <td>總計</td>
    <td>%(count)s</td>
    <td>%(Pass)s</td>
    <td>%(fail)s</td>
    <td>%(error)s</td>
    <td>透過率:%(passrate)s</td>
</tr>
</table>
""" # variables: (test_list, count, Pass, fail, error ,passrate)

    REPORT_CLASS_TMPL = r"""
<tr class='%(style)s'>
    <td>%(desc)s</td>
    <td class="text-center">%(count)s</td>
    <td class="text-center">%(Pass)s</td>
    <td class="text-center">%(fail)s</td>
    <td class="text-center">%(error)s</td>
    <td class="text-center"><a href="javascript:showClassDetail('%(cid)s',%(count)s)" class="detail" id='%(cid)s'>詳細</a></td>
</tr>
""" # variables: (style, desc, count, Pass, fail, error, cid)

    #有output內容的樣式,去掉原來JS效果,美化展示效果  -Findyou v0.8.2.3
    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'>
    <!--預設收起output資訊 -Findyou
    <button id='btn_%(tid)s' type="button"  class="btn-xs collapsed" data-toggle="collapse" data-target='#div_%(tid)s'>%(status)s</button>
    <div id='div_%(tid)s' class="collapse">  -->

    <!-- 預設展開output資訊 -Findyou -->
    <button id='btn_%(tid)s' type="button"  class="btn-xs" data-toggle="collapse" data-target='#div_%(tid)s'>%(status)s</button>
    <div id='div_%(tid)s' class="collapse in">
    <pre>
    %(script)s
    </pre>
    </div>
    </td>
</tr>
""" # variables: (tid, Class, style, desc, status)

    # 無output內容樣式改為button,按鈕效果為不可點選  -Findyou v0.8.2.3
    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'><button id='btn_%(tid)s' type="button"  class="btn-xs" disabled="disabled" data-toggle="collapse" data-target='#div_%(tid)s'>%(status)s</button></td>
</tr>
""" # variables: (tid, Class, style, desc, status)

    REPORT_TEST_OUTPUT_TMPL = r"""
%(id)s: %(output)s
""" # variables: (id, output)

    # ------------------------------------------------------------------------
    # ENDING
    #
    # 增加返回頂部按鈕  --Findyou
    ENDING_TMPL = """<div id='ending'> </div>
    <div style=" position:fixed;right:50px; bottom:30px; width:20px; height:20px;cursor:pointer">
    <a href="#"><span class="glyphicon glyphicon-eject" style = "font-size:30px;" aria-hidden="true">
    </span></a></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

        # Log is a list of Log in 4 tuple
        # (
        #   Log code (0: success; 1: fail; 2: error),
        #   TestCase object,
        #   Test output (byte string),
        #   stack trace,
        # )
        self.result = []
        #增加一個測試透過率 --Findyou
        self.passrate=float(0)


    def startTest(self, test):
        TestResult.startTest(self, test)
        # just one buffer for both stdout and stderr
        self.outputBuffer = 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):
        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')


class HTMLTestReportCN(Template_mixin):
    """
    """
    def __init__(self, stream=sys.stdout, verbosity=1,title=None,description=None,tester=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
        if tester is None:
            self.tester = self.DEFAULT_TESTER
        else:
            self.tester = tester

        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 >>sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)
        sys.stderr.write('\nTime Elapsed: %s' % (self.stopTime-self.startTime))
        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 not rmap.has_key(cls):
            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

    #替換測試結果status為透過率 --Findyou
    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 = []
        status.append('共 %s' % (result.success_count + result.failure_count + result.error_count))
        if result.success_count: status.append('透過 %s'    % result.success_count)
        if result.failure_count: status.append('失敗 %s' % result.failure_count)
        if result.error_count:   status.append('錯誤 %s'   % result.error_count  )
        if status:
            status = ','.join(status)
        # 合入Github:boafantasy程式碼
            if (result.success_count + result.failure_count + result.error_count) > 0:
                self.passrate = str("%.2f%%" % (float(result.success_count) / float(result.success_count + result.failure_count + result.error_count) * 100))
            else:
                self.passrate = "0.00 %"
        else:
            status = 'none'
        return [
            (u'測試人員', self.tester),
            (u'開始時間',startTime),
            (u'合計耗時',duration),
            (u'測試結果',status + ",透過率= "+self.passrate),
        ]


    def generateReport(self, test, result):
        report_attrs = self.getReportAttributes(result)
        generator = 'HTMLTestReportCN %s' % __version__
        stylesheet = self._generate_stylesheet()
        heading = self._generate_heading(report_attrs)
        report = self._generate_report(result)
        ending = self._generate_ending()
        output = self.HTML_TMPL % dict(
            title = saxutils.escape(self.title),
            generator = generator,
            stylesheet = stylesheet,
            heading = heading,
            report = report,
            ending = ending,
        )
        self.stream.write(output.encode('utf8'))


    def _generate_stylesheet(self):
        return self.STYLESHEET_TMPL

    #增加Tester顯示 -Findyou
    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),
            tester= saxutils.escape(self.tester),
        )
        return heading

    #生成報告  --Findyou新增註釋
    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 'warning' or nf > 0 and 'danger' or 'success',
                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),
            passrate =self.passrate,
        )
        return report


    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)
        # ID修改點為下劃線,支援Bootstrap摺疊展開特效 - Findyou v0.8.2.1
        #增加error分類 - Findyou v0.8.2.3
        tid = (n == 0 and 'p' or n == 1 and 'f' or 'e') + '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

        # utf-8 支援中文 - Findyou
         # o and e should be byte string because they are collected from stdout and stderr?
        if isinstance(o, str):
            # TODO: some problem with 'string_escape': it escape \n and mess up formating
            # uo = unicode(o.encode('string_escape'))
            try:
                uo = o
            except:
                uo = o.decode('utf-8')
        else:
            uo = o
        if isinstance(e, str):
            # TODO: some problem with 'string_escape': it escape \n and mess up formating
            # ue = unicode(e.encode('string_escape'))
            try:
                ue = e
            except:
                ue = e.decode('utf-8')
        else:
            ue = e

        script = self.REPORT_TEST_OUTPUT_TMPL % dict(
            id = tid,
            output = saxutils.escape(uo+ue),
        )

        row = tmpl % dict(
            tid = tid,
            Class = (n == 0 and 'hiddenRow' or 'none'),
            style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'passCase'),
            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 HTMLTestReportCN as the default test runner.
        # base class's testRunner parameter is not useful because it means
        # we have to instantiate HTMLTestReportCN before we know self.verbosity.
        if self.testRunner is None:
            self.testRunner = HTMLTestReportCN(verbosity=self.verbosity)
        unittest.TestProgram.runTests(self)

main = TestProgram

##############################################################################
# Executing this module from the command line
##############################################################################

if __name__ == "__main__":
    main(module=None)

【Config 部分】
定義配置檔案 config.ini

# -*- coding: utf-8 -*-
[HTTP]
scheme = http
baseurl = 127.0.0.1
port = 5000
timeout = 10.0

[DATABASE]
host = 10.181.79.156
port = 3306
user = root
passwd = root
database = interface
dbchar = utf8
table = interface_test

[EMAIL]
on_off = off
EMAIL_SERVICE = smtp.qq.com
EMAIL_PORT = 465
SENDER_ADDRESS = 364942727@qq.com
SENDER_PASSWORD = szkaushkeanabcde
RECEIVER_ADDRESS = 364942727@qq.com

對讀取配置檔案 config.ini 方法的封裝

# -*- coding: utf-8 -*-
"""
@File:readConfig.py
@E-mail:364942727@qq.com
@Time:2020/9/3 13:58 上午
@Author:Nobita
@Version:1.0
@Desciption:封裝讀取配置ini檔案
"""

import os
import configparser
import getpathInfo


class ReadConfig():
    def __init__(self):
        self.path = getpathInfo.get_Path()  # 呼叫例項化
        self.config_path = os.path.join(self.path, 'Config', 'Config.ini')  # 這句話是在path路徑下再加一級
        self.config = configparser.ConfigParser()  # 呼叫外部的讀取配置檔案的方法
        self.config.read(self.config_path, encoding='utf-8')

    def get_http(self, name):
        value = self.config.get('HTTP', name)
        return value

    def get_email(self, name):
        value = self.config.get('EMAIL', name)
        return value

    def get_mysql(self, name):  # 寫好,留以後備用。但是因為我們沒有對資料庫的操作,所以這個可以遮蔽掉
        value = self.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'))

定義介面用例是否執行的配置檔案

learning-API-test/test_login
learning-API-test/test_header
#learning-API-test/test_auth
#learning-API-test/test_menu

【learning-API-test 部分】
flask 開發的介面 demo,具體程式碼參考 github,這裡不做詳細介紹。

【Log 部分】
資料夾 logs 用來儲存 log 日誌的檔案
日誌輸出內容預覽:

【框架流程圖部分】
存放此介面框架的流程圖,檔名:此框架流程圖.xmind

【Report 部分】
存放測試結束後生成的 html 介面測試報告,檔名:report.html

【TestCase 部分】
用來存放各個介面的測試用例。這裡我舉兩個介面栗子。
[ 栗子①:/login ]

# -*- coding: utf-8 -*-
"""
@File:test_login.py
@E-mail:364942727@qq.com
@Time:2020/9/3 9:28 下午
@Author:Nobita
@Version:1.0
@Desciption:/login介面的測試用例及斷言
"""

import json
import unittest
import paramunittest
from Common import readExcel, geturlParams
from Common.Assert import Assertions
from Common.Request import RunMain

url = geturlParams.geturlParams().get_Url()  # 呼叫我們的geturlParams獲取我們拼接的URL
login_xls = readExcel.readExcel().get_xls('learning-API-test_Case.xlsx', 'login')


@user55trized(*login_xls)
class test_learning_API(unittest.TestCase):

    def setParameters(self, case_name, path, headers, data, method):
        """
        set params
        :param case_name:
        :param path
        :param headers
        :param data
        :param method
        :return:
        """
        self.case_name = case_name
        self.path = path
        self.headers = headers
        self.data = data
        self.method = method

    def description(self):
        """
        test report description
        :return:
        """
        print(self.case_name)

    def setUp(self):
        """

        :return:
        """
        print("測試開始,測試用例名稱:{}".format(self.case_name))

    def test_login(self):
        self.checkResult()

    def tearDown(self):
        print("測試結束,輸出log完結\n\n")

    def checkResult(self):
        """
        check test Log
        :return:
        """
        request_url = url + self.path
        new_data = json.loads(self.data)  # 將Excel中提取的data從字串轉換成字典形式入參
        info = RunMain().run_main(method=self.method, url=request_url,
                                  data=new_data)  # 根據Excel中的method呼叫run_main來進行requests請求,並拿到響應
        print('介面響應報文:{}'.format(info))  # 在report中列印響應報文
        # 對響應結果進行斷言
        if self.case_name == 'login_pass':
            Assertions().assert_code(info['code'], 10200)
            Assertions().assert_in_text(info['message'], 'success')
        if self.case_name == 'login_error':
            Assertions().assert_code(info['code'], 10104)
            Assertions().assert_in_text(info['message'], 'error')
        if self.case_name == 'login_null':
            Assertions().assert_code(info['code'], 10103)
            Assertions().assert_in_text(info['message'], 'null')


if __name__ == "__main__":
    # unittest.main()
    pass

[ 栗子②:/header ]

# -*- coding: utf-8 -*-
"""
@File:test_header.py
@E-mail:364942727@qq.com
@Time:2020/9/3 11:28 下午
@Author:Nobita
@Version:1.0
@Desciption:/header介面的測試用例及斷言
"""

import json
import unittest
import paramunittest
from Common import readExcel, geturlParams
from Common.Assert import Assertions
from Common.Request import RunMain

url = geturlParams.geturlParams().get_Url()  # 呼叫我們的geturlParams獲取我們拼接的URL
login_xls = readExcel.readExcel().get_xls('learning-API-test_Case.xlsx', 'header')


@user62trized(*login_xls)
class test_learning_API(unittest.TestCase):

    def setParameters(self, case_name, path, headers, data, method):
        """
        set params
        :param case_name:
        :param path
        :param headers
        :param data
        :param method
        :return:
        """
        self.case_name = case_name
        self.path = path
        self.headers = headers
        self.data = data
        self.method = method

    def description(self):
        """
        test report description
        :return:
        """
        print(self.case_name)

    def setUp(self):
        """

        :return:
        """
        print("測試開始,測試用例名稱:{}".format(self.case_name))

    def test_header(self):
        self.checkResult()

    def tearDown(self):
        print("測試結束,輸出log完結\n\n")

    def checkResult(self):
        """
        check test Log
        :return:
        """
        request_url = url + self.path
        headers = self.headers
        new_headers = json.loads(headers)
        info = RunMain().run_main(method=self.method, url=request_url, headers=new_headers
                                  )  # 根據Excel中的method呼叫run_main來進行requests請求,並拿到響應
        print('介面響應報文:{}'.format(info))  # 在report中列印響應報文
        # 對響應結果進行斷言
        if self.case_name == 'header_pass':
            Assertions().assert_code(info['code'], 10200)
            Assertions().assert_in_text(info['message'], 'ok')


if __name__ == "__main__":
    # unittest.main()
    pass

【TestFile 部分】
用來存放介面專案的測試資料,採用 Excel 方式管理,具體內容參考 github 上的檔案內容。

【getpathInfo 部分】
用來獲取專案的檔案路徑,一般都放在工程根目錄。

# -*- coding: utf-8 -*-
"""
@File:getpathInfo.py
@E-mail:364942727@qq.com
@Time:2020/9/3 7:58 下午
@Author:Nobita
@Version:1.0
@Desciption:獲取專案的檔案路徑
"""

import os


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


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

【requirements.txt 部分】
整個專案所需要的依賴包及精確的版本資訊。

APScheduler==3.6.3
certifi==2020.6.20
chardet==3.0.4
click==7.1.2
Flask==1.0.2
idna==2.8
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
ParamUnittest==0.2
pycryptodome==3.7.3
PyEmail==0.0.1
pytz==2020.1
requests==2.22.0
six==1.15.0
tzlocal==2.1
urllib3==1.25.10
Werkzeug==1.0.1
xlrd==1.2.0

【RunAll.py 部分】
對專案所有功能模組呼叫的封裝。

# -*- coding: utf-8 -*-
"""
@File:RunAll.py
@E-mail:364942727@qq.com
@Time:2020/9/6 17:58 下午
@Author:Nobita
@Version:1.0
@Desciption:專案總執行指令碼
"""

import os
import Common.HTMLTestRunner as HTMLTestRunner
import getpathInfo
import unittest
from Config import readConfig
from Common.SendEmail import SendEmail
from Common.Log import logger

send_mail = SendEmail()
path = getpathInfo.get_Path()
report_path = os.path.join(path, 'Report')
resultPath = os.path.join(report_path, "report.html")  # Log/report.html
on_off = readConfig.ReadConfig().get_email('on_off')
log = logger


class AllTest:  # 定義一個類AllTest
    def __init__(self):  # 初始化一些引數和數
        self.caseListFile = os.path.join(path, "Config", "caselist.txt")  # 配置執行哪些測試檔案的配置檔案路徑
        self.caseFile = os.path.join(path, "TestCase")  # 真正的測試斷言檔案路徑
        self.caseList = []
        log.info('測試報告的路徑:{},執行用例配置檔案路徑:{}'.format(resultPath, self.caseListFile))  # 將檔案路徑輸入到日誌,方便定位檢視問題

    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()
        log.info('執行的測試用例:{}'.format(self.caseList))

    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
            if suit is not None:  # 判斷test_suite是否為空
                fp = open(resultPath, 'wb')  # 開啟Report/report.html測試報告檔案,如果不存在就建立
                # 呼叫HTMLTestRunner
                runner = HTMLTestRunner.HTMLTestReportCN(stream=fp, tester='Shengkai Chen', title='Learning_API 介面測試報告',
                                                         description=None)
                runner.run(suit)
            else:
                print("Have no case to test.")
                log.info('沒有可以執行的測試用例,請檢視用例配置檔案caselist.txt')
        except Exception as ex:
            print(str(ex))
            log.info('{}'.format(str(ex)))

        finally:
            print("*********TEST END*********")
        # 判斷郵件傳送的開關
        if on_off == 'on':
            SendEmail().send_email()
        else:
            print("郵件傳送開關配置關閉,請開啟開關後可正常自動傳送測試報告")


if __name__ == '__main__':
    AllTest().run()

【README.md】
介面測試框架專案的詳細介紹文件。具體內容參考 github 上的檔案內容。

結束語

這個週末沒有睡懶覺。。。整理了這個介面框架 demo 分享給入門的新人,

更多功能需要結合生產上的業務需求進行開發挖掘。

學習和工作是一個循序漸進,不斷肯定以及不斷否定自我的過程。

希望我們能在此過程中像程式碼一樣迭代自我,加油!

如果方便的話,在 github 上給我個小星星,在這裡提前跪謝大佬了,麼麼噠。

github 原始碼下載地址:https://github.com/charseki/API_Auto_Test

相關文章