接口测试 一个简单的接口测试框架 demo

Charseki.Chen · 2020年09月06日 · 最后由 Charseki.Chen 回复于 2020年09月07日 · 657 次阅读

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

共收到 6 条回复 时间 点赞

你好楼主,我想请问下 其中的测试平台是直接使用的 django + xadmin 后台一套写的嘛,还是说是单独的前端展示页面 (及路由) 并非用的 xadmin 本身的后台管理系统改造而来

柒意 回复

前后端分离的,Flask+Vue+Nginx+Gunicorn 部署的平台。

Charseki.Chen 回复

👌,谢谢回答,学习了

有点类似于 UI 自动化的 PO 模式写出来的框架。将各个功能分到了对应的文件里面,直接 UI 自动化跟这个思路差不多,接口也用这个思路写过

是的,把代码和测试数据尽可能的分离。让测试代码可读性更好,可维护性更强,复用性更高。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册