精华内容
下载资源
问答
  • 完整的框架源码下载...一、Python+unittest+requests+HTMLTestRunner 完整的接口自动化测试框架搭建_00——框架结构简解 首先配置好开发环境,下载安装Python并...

    完整的框架源码下载 https://gitee.com/submi_to/interfaceTest/tree/develop/,欢迎添加我的微信,相互学习探讨~1305618688,qq交流群:849102042

    一、Python+unittest+requests+HTMLTestRunner 完整的接口自动化测试框架搭建_00——框架结构简解

     首先配置好开发环境,下载安装Python并下载安装pycharm,在pycharm中创建项目功能目录。如果不会的可以百度Google一下,该内容网上的讲解还是比较多比较全的!

    大家可以先简单了解下该项目的目录结构介绍,后面会针对每个文件有详细注解和代码。

    common:

    ——configDb.py:这个文件主要编写数据库连接池的相关内容,本项目暂未考虑使用数据库来存储读取数据,此文件可忽略,或者不创建。本人是留着以后如果有相关操作时,方便使用。

    ——configEmail.py:这个文件主要是配置发送邮件的主题、正文等,将测试报告发送并抄送到相关人邮箱的逻辑。

    ——configHttp.py:这个文件主要来通过get、post、put、delete等方法来进行http请求,并拿到请求响应。

    ——HTMLTestRunner.py:主要是生成测试报告相关

    ——Log.py:调用该类的方法,用来打印生成日志

    result:

    ——logs:生成的日志文件

    ——report.html:生成的测试报告

    testCase:

    ——test01case.py:读取userCase.xlsx中的用例,使用unittest来进行断言校验

    testFile/case:

    ——userCase.xlsx:对下面test_api.py接口服务里的接口,设计了三条简单的测试用例,如参数为null,参数不正确等

    caselist.txt:配置将要执行testCase目录下的哪些用例文件,前加#代表不进行执行。当项目过于庞大,用例足够多的时候,我们可以通过这个开关,来确定本次执行哪些接口的哪些用例。

    config.ini:数据库、邮箱、接口等的配置项,用于方便的调用读取。

    getpathInfo.py:获取项目绝对路径

    geturlParams.py:获取接口的URL、参数、method等

    readConfig.py:读取配置文件的方法,并返回文件中内容

    readExcel.py:读取Excel的方法

    runAll.py:开始执行接口自动化,项目工程部署完毕后直接运行该文件即可

    test_api.py:自己写的提供本地测试的接口服务

    test_sql.py:测试数据库连接池的文件,本次项目未用到数据库,可以忽略

    二、Python+unittest+requests+HTMLTestRunner完整的接口自动化测试框架搭建_01——测试接口服务

    首先,我们想搭建一个接口自动化测试框架,前提我们必须要有一个可支持测试的接口服务。有人可能会说,现在我们的环境不管测试环境,还是生产环境有现成的接口。但是,一般工作环境中的接口,不太满足我们框架的各种条件。举例如,接口a可能是get接口b可能又是post,等等等等。因此我决定自己写一个简单的接口!用于我们这个框架的测试!

    按第一讲的目录创建好文件,打开test_api.py,写入如下代码

    import flask
    import json
    from flask import request
    
    '''
    flask: web框架,通过flask提供的装饰器@server.route()将普通函数转换为服
    '''
    # 创建一个服务,把当前这个python文件当做一个服务
    server = flask.Flask(__name__)
    # @server.route()可以将普通函数转变为服务 登录接口的路径、请求方式
    @server.route('/login', methods=['get', 'post'])
    def login():
        # 获取通过url请求传参的数据
        username = request.values.get('name')
        # 获取url请求传的密码,明文
        pwd = request.values.get('pwd')
        # 判断用户名、密码都不为空
        if username and pwd:
            if username == 'xiaoming' and pwd == '111':
                resu = {'code': 200, 'message': '登录成功'}
                return json.dumps(resu, ensure_ascii=False)  # 将字典转换字符串
            else:
                resu = {'code': -1, 'message': '账号密码错误'}
                return json.dumps(resu, ensure_ascii=False)
        else:
            resu = {'code': 10001, 'message': '参数不能为空!'}
            return json.dumps(resu, ensure_ascii=False)
    
    if __name__ == '__main__':
        server.run(debug=True, port=8888, host='127.0.0.1')
    
    

    执行test_api.py,在浏览器中输入http://127.0.0.1:8888/login?name=xiaoming&pwd=11199回车,验证我们的接口服务是否正常~

    变更我们的参数,查看不同的响应结果确认接口服务一切正常

    三、Python+unittest+requests+HTMLTestRunner完整的接口自动化测试框架搭建_02——配置文件读取

    在我们第二讲中,我们已经通过flask这个web框架创建好了我们用于测试的接口服务,因此我们可以把这个接口抽出来一些参数放到配置文件,然后通过一个读取配置文件的方法,方便后续的使用。同样还有邮件的相关配置~

    按第一讲的目录创建好config.ini文件,打开该文件写入如下:

    # -*- coding: utf-8 -*-
    [HTTP]
    scheme = http
    baseurl = 127.0.0.1
    port = 8888
    timeout = 10.0
    
    
    
    [EMAIL]
    on_off = on;
    subject = 接口自动化测试报告
    app = Outlook
    addressee = songxiaobao@qq.com
    cc = zhaobenshan@qq.com

    在HTTP中,协议http,baseURL,端口,超时时间。

    在邮件中on_off是设置的一个开关,=on打开,发送邮件,=其他不发送邮件。subject邮件主题,addressee收件人,cc抄送人。

    在我们编写readConfig.py文件前,我们先写一个获取项目某路径下某文件绝对路径的一个方法。按第一讲的目录结构创建好getpathInfo.py,打开该文件

    import os
    
    def get_Path():
        path = os.path.split(os.path.realpath(__file__))[0]
        return path
    
    if __name__ == '__main__':# 执行该文件,测试下是否OK
        print('测试路径是否OK,路径为:', get_Path())
    

    填写如上代码并执行后,查看输出结果,打印出了该项目的绝对路径:

    继续往下走,同理,按第一讲目录创建好readConfig.py文件,打开该文件,以后的章节不在累赘

    import os
    import configparser
    import getpathInfo#引入我们自己的写的获取路径的类
    
    path = getpathInfo.get_Path()#调用实例化,还记得这个类返回的路径为C:\Users\songlihui\PycharmProjects\dkxinterfaceTest
    config_path = os.path.join(path, 'config.ini')#这句话是在path路径下再加一级,最后变成C:\Users\songlihui\PycharmProjects\dkxinterfaceTest\config.ini
    config = configparser.ConfigParser()#调用外部的读取配置文件的方法
    config.read(config_path, encoding='utf-8')
    
    class ReadConfig():
    
        def get_http(self, name):
            value = config.get('HTTP', name)
            return value
        def get_email(self, name):
            value = config.get('EMAIL', name)
            return value
        def get_mysql(self, name):#写好,留以后备用。但是因为我们没有对数据库的操作,所以这个可以屏蔽掉
            value = config.get('DATABASE', name)
            return value
    
    
    if __name__ == '__main__':#测试一下,我们读取配置文件的方法是否可用
        print('HTTP中的baseurl值为:', ReadConfig().get_http('baseurl'))
        print('EMAIL中的开关on_off值为:', ReadConfig().get_email('on_off'))

    执行下readConfig.py,查看数据是否正确

    一切OK

    四、Python+unittest+requests+HTMLTestRunner完整的接口自动化测试框架搭建_03——读取Excel中的case

    配置文件写好了,接口我们也有了,然后我们来根据我们的接口设计我们简单的几条用例。首先在前两讲中我们写了一个我们测试的接口服务,针对这个接口服务存在三种情况的校验。正确的用户名和密码,账号密码错误和账号密码为空

    我们根据上面的三种情况,将对这个接口的用例写在一个对应的单独文件中testFile\case\userCase.xlsx ,userCase.xlsx内容如下:

    紧接着,我们有了用例设计的Excel了,我们要对这个Excel进行数据的读取操作,继续往下,我们创建readExcel.py文件

    import os
    import getpathInfo# 自己定义的内部类,该类返回项目的绝对路径
    #调用读Excel的第三方库xlrd
    from xlrd import open_workbook
    # 拿到该项目所在的绝对路径
    path = getpathInfo.get_Path()
    
    class readExcel():
        def get_xls(self, xls_name, sheet_name):# xls_name填写用例的Excel名称 sheet_name该Excel的sheet名称
            cls = []
            # 获取用例文件路径
            xlsPath = os.path.join(path, "testFile", 'case', xls_name)
            file = open_workbook(xlsPath)# 打开用例Excel
            sheet = file.sheet_by_name(sheet_name)#获得打开Excel的sheet
            # 获取这个sheet内容行数
            nrows = sheet.nrows
            for i in range(nrows):#根据行数做循环
                if sheet.row_values(i)[0] != u'case_name':#如果这个Excel的这个sheet的第i行的第一列不等于case_name那么我们把这行的数据添加到cls[]
                    cls.append(sheet.row_values(i))
            return cls
    if __name__ == '__main__':#我们执行该文件测试一下是否可以正确获取Excel中的值
        print(readExcel().get_xls('userCase.xlsx', 'login'))
        print(readExcel().get_xls('userCase.xlsx', 'login')[0][1])
        print(readExcel().get_xls('userCase.xlsx', 'login')[1][2])

    结果为:

    完全正确~

    五、Python+unittest+requests+HTMLTestRunner完整的接口自动化测试框架搭建_04——requests请求

    配置文件有了,读取配置文件有了,用例有了,读取用例有了,我们的接口服务有了,我们是不是该写对某个接口进行http请求了,这时候我们需要使用pip install requests来安装第三方库,在common下configHttp.py,configHttp.py的内容如下:

    import requests
    import json
    
    
    class RunMain():
    
        def send_post(self, url, data):  # 定义一个方法,传入需要的参数url和data
            # 参数必须按照url、data顺序传入
            result = requests.post(url=url, data=data).json()  # 因为这里要封装post方法,所以这里的url和data值不能写死
            res = json.dumps(result, ensure_ascii=False, sort_keys=True, indent=2)
            return res
    
        def send_get(self, url, data):
            result = requests.get(url=url, params=data).json()
            res = json.dumps(result, ensure_ascii=False, sort_keys=True, indent=2)
            return res
    
        def run_main(self, method, url=None, data=None):  # 定义一个run_main函数,通过传过来的method来进行不同的get或post请求
            result = None
            if method == 'post':
                result = self.send_post(url, data)
            elif method == 'get':
                result = self.send_get(url, data)
            else:
                print("method值错误!!!")
            return result
    
    
    if __name__ == '__main__':  # 通过写死参数,来验证我们写的请求是否正确
        result1 = RunMain().run_main('post', 'http://127.0.0.1:8888/login', {'name': 'xiaoming','pwd':'111'})
        result2 = RunMain().run_main('get', 'http://127.0.0.1:8888/login', 'name=xiaoming&pwd=111')
        print(result1)
        print(result2)

    执行该文件,验证结果正确性:

    我们发现和浏览器中进行请求该接口,得到的结果一致,说明没有问题,一切OK

    六、Python+unittest+requests+HTMLTestRunner完整的接口自动化测试框架搭建_05——参数动态化

    在上一讲中,我们写了针对我们的接口服务,设计的三种测试用例,使用写死的参数(result = RunMain().run_main('post', 'http://127.0.0.1:8888/login', 'name=xiaoming&pwd='))来进行requests请求。本讲中我们写一个类,来用于分别获取这些参数,来第一讲的目录创建geturlParams.py,geturlParams.py文件中的内容如下:

    import readConfig as readConfig
    
    readconfig = readConfig.ReadConfig()
    
    class geturlParams():# 定义一个方法,将从配置文件中读取的进行拼接
        def get_Url(self):
            new_url = readconfig.get_http('scheme') + '://' + readconfig.get_http('baseurl') + ':8888' + '/login' + '?'
            #logger.info('new_url'+new_url)
            return new_url
    
    if __name__ == '__main__':# 验证拼接后的正确性
        print(geturlParams().get_Url())

    通过将配置文件中的进行拼接,拼接后的结果:http://127.0.0.1:8888/login?和我们请求的一致

    七、Python+unittest+requests+HTMLTestRunner完整的接口自动化测试框架搭建_06——unittest断言

    以上的我们都准备好了,剩下的该写我们的unittest断言测试case了,在testCase下创建test01case.py文件,文件中内容如下:

    import json
    import unittest
    from common.configHttp import RunMain
    import paramunittest
    import geturlParams
    import urllib.parse
    # import pythoncom
    import readExcel
    # pythoncom.CoInitialize()
    
    url = geturlParams.geturlParams().get_Url()# 调用我们的geturlParams获取我们拼接的URL
    login_xls = readExcel.readExcel().get_xls('userCase.xlsx', 'login')
    
    @paramunittest.parametrized(*login_xls)
    class testUserLogin(unittest.TestCase):
        def setParameters(self, case_name, path, query, method):
            """
            set params
            :param case_name:
            :param path
            :param query
            :param method
            :return:
            """
            self.case_name = str(case_name)
            self.path = str(path)
            self.query = str(query)
            self.method = str(method)
    
        def description(self):
            """
            test report description
            :return:
            """
            self.case_name
    
        def setUp(self):
            """
    
            :return:
            """
            print(self.case_name+"测试开始前准备")
    
        def test01case(self):
            self.checkResult()
    
        def tearDown(self):
            print("测试结束,输出log完结\n\n")
    
        def checkResult(self):# 断言
            """
            check test result
            :return:
            """
            url1 = "http://www.xxx.com/login?"
            new_url = url1 + self.query
            data1 = dict(urllib.parse.parse_qsl(urllib.parse.urlsplit(new_url).query))# 将一个完整的URL中的name=&pwd=转换为{'name':'xxx','pwd':'bbb'}
            info = RunMain().run_main(self.method, url, data1)# 根据Excel中的method调用run_main来进行requests请求,并拿到响应
            ss = json.loads(info)# 将响应转换为字典格式
            if self.case_name == 'login':# 如果case_name是login,说明合法,返回的code应该为200
                self.assertEqual(ss['code'], 200)
            if self.case_name == 'login_error':# 同上
                self.assertEqual(ss['code'], -1)
            if self.case_name == 'login_null':# 同上
                self.assertEqual(ss['code'], 10001)

    八、Python+unittest+requests+HTMLTestRunner完整的接口自动化测试框架搭建_07——HTMLTestRunner

    按我的目录结构,在common下创建HTMLTestRunner.py文件,内容如下:

    # -*- coding: utf-8 -*-
    """
    A TestRunner for use with the Python unit testing framework. It
    generates a HTML report to show the result at a glance.
    The simplest way to use this is to invoke its main method. E.g.
        import unittest
        import HTMLTestRunner
        ... define your tests ...
        if __name__ == '__main__':
            HTMLTestRunner.main()
    For more customization options, instantiates a HTMLTestRunner object.
    HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.
        # output to a file
        fp = file('my_report.html', 'wb')
        runner = HTMLTestRunner.HTMLTestRunner(
                    stream=fp,
                    title='My unit test',
                    description='This demonstrates the report output by HTMLTestRunner.'
                    )
        # Use an external stylesheet.
        # See the Template_mixin class for more customizable options
        runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'
        # run the test
        runner.run(my_test_suite)
    ------------------------------------------------------------------------
    Copyright (c) 2004-2007, Wai Yip Tung
    All rights reserved.
    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are
    met:
    * Redistributions of source code must retain the above copyright notice,
      this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name Wai Yip Tung nor the names of its contributors may be
      used to endorse or promote products derived from this software without
      specific prior written permission.
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
    OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
    EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
    PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
    PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
    LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
    NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    """
    
    # URL: http://tungwaiyip.info/software/HTMLTestRunner.html
    
    __author__ = "Wai Yip Tung"
    __version__ = "0.9.1"
    
    """
    Change History
    Version 0.9.1
    * 用Echarts添加执行情况统计图 (灰蓝)
    Version 0.9.0
    * 改成Python 3.x (灰蓝)
    Version 0.8.3
    * 使用 Bootstrap稍加美化 (灰蓝)
    * 改为中文 (灰蓝)
    Version 0.8.2
    * Show output inline instead of popup window (Viorel Lupu).
    Version in 0.8.1
    * Validated XHTML (Wolfgang Borgert).
    * Added description of test classes and test cases.
    Version in 0.8.0
    * Define Template_mixin class for customization.
    * Workaround a IE 6 bug that it does not treat <script> block as CDATA.
    Version in 0.7.1
    * Back port to Python 2.3 (Frank Horowitz).
    * Fix missing scroll bars in detail log (Podi).
    """
    
    # TODO: color stderr
    # TODO: simplify javascript using ,ore than 1 class in the class attribute?
    
    import datetime
    import sys
    import io
    import time
    import unittest
    from xml.sax import saxutils
    
    
    # ------------------------------------------------------------------------
    # The redirectors below are used to capture output during testing. Output
    # sent to sys.stdout and sys.stderr are automatically captured. However
    # in some cases sys.stdout is already cached before HTMLTestRunner is
    # invoked (e.g. calling logging.basicConfig). In order to capture those
    # output, use the redirectors for the cached stream.
    #
    # e.g.
    #   >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
    #   >>>
    
    class OutputRedirector(object):
        """ Wrapper to redirect stdout or stderr """
    
        def __init__(self, fp):
            self.fp = fp
    
        def write(self, s):
            self.fp.write(s)
    
        def writelines(self, lines):
            self.fp.writelines(lines)
    
        def flush(self):
            self.fp.flush()
    
    
    stdout_redirector = OutputRedirector(sys.stdout)
    stderr_redirector = OutputRedirector(sys.stderr)
    
    
    # ----------------------------------------------------------------------
    # Template
    
    
    class Template_mixin(object):
        """
        Define a HTML template for report customerization and generation.
        Overall structure of an HTML report
        HTML
        +------------------------+
        |<html>                  |
        |  <head>                |
        |                        |
        |   STYLESHEET           |
        |   +----------------+   |
        |   |                |   |
        |   +----------------+   |
        |                        |
        |  </head>               |
        |                        |
        |  <body>                |
        |                        |
        |   HEADING              |
        |   +----------------+   |
        |   |                |   |
        |   +----------------+   |
        |                        |
        |   REPORT               |
        |   +----------------+   |
        |   |                |   |
        |   +----------------+   |
        |                        |
        |   ENDING               |
        |   +----------------+   |
        |   |                |   |
        |   +----------------+   |
        |                        |
        |  </body>               |
        |</html>                 |
        +------------------------+
        """
    
        STATUS = {
            0: u'通过',
            1: u'失败',
            2: u'错误',
        }
    
        DEFAULT_TITLE = 'Unit Test Report'
        DEFAULT_DESCRIPTION = ''
    
        # ------------------------------------------------------------------------
        # HTML Template
    
        HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>%(title)s</title>
        <meta name="generator" content="%(generator)s"/>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    
        <link href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet">
        <script src="https://cdn.bootcss.com/echarts/3.8.5/echarts.common.min.js"></script>
        <!-- <script type="text/javascript" src="js/echarts.common.min.js"></script> -->
    
        %(stylesheet)s
    
    </head>
    <body>
        <script language="javascript" type="text/javascript"><!--
        output_list = Array();
        /* level - 0:Summary; 1:Failed; 2:All */
        function showCase(level) {
            trs = document.getElementsByTagName("tr");
            for (var i = 0; i < trs.length; i++) {
                tr = trs[i];
                id = tr.id;
                if (id.substr(0,2) == 'ft') {
                    if (level < 1) {
                        tr.className = 'hiddenRow';
                    }
                    else {
                        tr.className = '';
                    }
                }
                if (id.substr(0,2) == 'pt') {
                    if (level > 1) {
                        tr.className = '';
                    }
                    else {
                        tr.className = 'hiddenRow';
                    }
                }
            }
        }
        function showClassDetail(cid, count) {
            var id_list = Array(count);
            var toHide = 1;
            for (var i = 0; i < count; i++) {
                tid0 = 't' + cid.substr(1) + '.' + (i+1);
                tid = 'f' + tid0;
                tr = document.getElementById(tid);
                if (!tr) {
                    tid = 'p' + tid0;
                    tr = document.getElementById(tid);
                }
                id_list[i] = tid;
                if (tr.className) {
                    toHide = 0;
                }
            }
            for (var i = 0; i < count; i++) {
                tid = id_list[i];
                if (toHide) {
                    document.getElementById('div_'+tid).style.display = 'none'
                    document.getElementById(tid).className = 'hiddenRow';
                }
                else {
                    document.getElementById(tid).className = '';
                }
            }
        }
        function showTestDetail(div_id){
            var details_div = document.getElementById(div_id)
            var displayState = details_div.style.display
            // alert(displayState)
            if (displayState != 'block' ) {
                displayState = 'block'
                details_div.style.display = 'block'
            }
            else {
                details_div.style.display = 'none'
            }
        }
        function html_escape(s) {
            s = s.replace(/&/g,'&amp;');
            s = s.replace(/</g,'&lt;');
            s = s.replace(/>/g,'&gt;');
            return s;
        }
        /* obsoleted by detail in <div>
        function showOutput(id, name) {
            var w = window.open("", //url
                            name,
                            "resizable,scrollbars,status,width=800,height=450");
            d = w.document;
            d.write("<pre>");
            d.write(html_escape(output_list[id]));
            d.write("\n");
            d.write("<a href='javascript:window.close()'>close</a>\n");
            d.write("</pre>\n");
            d.close();
        }
        */
        --></script>
        <div id="div_base">
            %(heading)s
            %(report)s
            %(ending)s
            %(chart_script)s
        </div>
    </body>
    </html>
    """  # variables: (title, generator, stylesheet, heading, report, ending, chart_script)
    
        ECHARTS_SCRIPT = """
        <script type="text/javascript">
            // 基于准备好的dom,初始化echarts实例
            var myChart = echarts.init(document.getElementById('chart'));
            // 指定图表的配置项和数据
            var option = {
                title : {
                    text: '测试执行情况',
                    x:'center'
                },
                tooltip : {
                    trigger: 'item',
                    formatter: "{a} <br/>{b} : {c} ({d}%%)"
                },
                color: ['#95b75d', 'grey', '#b64645'],
                legend: {
                    orient: 'vertical',
                    left: 'left',
                    data: ['通过','失败','错误']
                },
                series : [
                    {
                        name: '测试执行情况',
                        type: 'pie',
                        radius : '60%%',
                        center: ['50%%', '60%%'],
                        data:[
                            {value:%(Pass)s, name:'通过'},
                            {value:%(fail)s, name:'失败'},
                            {value:%(error)s, name:'错误'}
                        ],
                        itemStyle: {
                            emphasis: {
                                shadowBlur: 10,
                                shadowOffsetX: 0,
                                shadowColor: 'rgba(0, 0, 0, 0.5)'
                            }
                        }
                    }
                ]
            };
            // 使用刚指定的配置项和数据显示图表。
            myChart.setOption(option);
        </script>
        """  # variables: (Pass, fail, error)
    
        # ------------------------------------------------------------------------
        # Stylesheet
        #
        # alternatively use a <link> for external style sheet, e.g.
        #   <link rel="stylesheet" href="$url" type="text/css">
    
        STYLESHEET_TMPL = """
    <style type="text/css" media="screen">
        body        { font-family: Microsoft YaHei,Consolas,arial,sans-serif; font-size: 80%; }
        table       { font-size: 100%; }
        pre         { white-space: pre-wrap;word-wrap: break-word; }
        /* -- heading ---------------------------------------------------------------------- */
        h1 {
            font-size: 16pt;
            color: gray;
        }
        .heading {
            margin-top: 0ex;
            margin-bottom: 1ex;
        }
        .heading .attribute {
            margin-top: 1ex;
            margin-bottom: 0;
        }
        .heading .description {
            margin-top: 2ex;
            margin-bottom: 3ex;
        }
        /* -- css div popup ------------------------------------------------------------------------ */
        a.popup_link {
        }
        a.popup_link:hover {
            color: red;
        }
        .popup_window {
            display: none;
            position: relative;
            left: 0px;
            top: 0px;
            /*border: solid #627173 1px; */
            padding: 10px;
            /*background-color: #E6E6D6; */
            font-family: "Lucida Console", "Courier New", Courier, monospace;
            text-align: left;
            font-size: 8pt;
            /* width: 500px;*/
        }
        }
        /* -- report ------------------------------------------------------------------------ */
        #show_detail_line {
            margin-top: 3ex;
            margin-bottom: 1ex;
        }
        #result_table {
            width: 99%;
        }
        #header_row {
            font-weight: bold;
            color: #303641;
            background-color: #ebebeb;
        }
        #total_row  { font-weight: bold; }
        .passClass  { background-color: #bdedbc; }
        .failClass  { background-color: #ffefa4; }
        .errorClass { background-color: #ffc9c9; }
        .passCase   { color: #6c6; }
        .failCase   { color: #FF6600; font-weight: bold; }
        .errorCase  { color: #c00; font-weight: bold; }
        .hiddenRow  { display: none; }
        .testcase   { margin-left: 2em; }
        /* -- ending ---------------------------------------------------------------------- */
        #ending {
        }
        #div_base {
                    position:absolute;
                    top:0%;
                    left:5%;
                    right:5%;
                    width: auto;
                    height: auto;
                    margin: -15px 0 0 0;
        }
    </style>
    """
    
        # ------------------------------------------------------------------------
        # Heading
        #
    
        HEADING_TMPL = """
        <div class='page-header'>
            <h1>%(title)s</h1>
        %(parameters)s
        </div>
        <div style="float: left;width:50%%;"><p class='description'>%(description)s</p></div>
        <div id="chart" style="width:50%%;height:400px;float:left;"></div>
    """  # variables: (title, parameters, description)
    
        HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
    """  # variables: (name, value)
    
        # ------------------------------------------------------------------------
        # Report
        #
    
        REPORT_TMPL = u"""
        <div class="btn-group btn-group-sm">
            <button class="btn btn-default" onclick='javascript:showCase(0)'>总结</button>
            <button class="btn btn-default" onclick='javascript:showCase(1)'>失败</button>
            <button class="btn btn-default" onclick='javascript:showCase(2)'>全部</button>
        </div>
        <p></p>
        <table id='result_table' class="table table-bordered">
            <colgroup>
                <col align='left' />
                <col align='right' />
                <col align='right' />
                <col align='right' />
                <col align='right' />
                <col align='right' />
            </colgroup>
            <tr id='header_row'>
                <td>测试套件/测试用例</td>
                <td>总数</td>
                <td>通过</td>
                <td>失败</td>
                <td>错误</td>
                <td>查看</td>
            </tr>
            %(test_list)s
            <tr id='total_row'>
                <td>总计</td>
                <td>%(count)s</td>
                <td>%(Pass)s</td>
                <td>%(fail)s</td>
                <td>%(error)s</td>
                <td>&nbsp;</td>
            </tr>
        </table>
    """  # variables: (test_list, count, Pass, fail, error)
    
        REPORT_CLASS_TMPL = u"""
        <tr class='%(style)s'>
            <td>%(desc)s</td>
            <td>%(count)s</td>
            <td>%(Pass)s</td>
            <td>%(fail)s</td>
            <td>%(error)s</td>
            <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">详情</a></td>
        </tr>
    """  # variables: (style, desc, count, Pass, fail, error, cid)
    
        REPORT_TEST_WITH_OUTPUT_TMPL = r"""
    <tr id='%(tid)s' class='%(Class)s'>
        <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
        <td colspan='5' align='center'>
        <!--css div popup start-->
        <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
            %(status)s</a>
        <div id='div_%(tid)s' class="popup_window">
            <pre>%(script)s</pre>
        </div>
        <!--css div popup end-->
        </td>
    </tr>
    """  # variables: (tid, Class, style, desc, status)
    
        REPORT_TEST_NO_OUTPUT_TMPL = r"""
    <tr id='%(tid)s' class='%(Class)s'>
        <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
        <td colspan='5' align='center'>%(status)s</td>
    </tr>
    """  # variables: (tid, Class, style, desc, status)
    
        REPORT_TEST_OUTPUT_TMPL = r"""%(id)s: %(output)s"""  # variables: (id, output)
    
        # ------------------------------------------------------------------------
        # ENDING
        #
    
        ENDING_TMPL = """<div id='ending'>&nbsp;</div>"""
    
    
    # -------------------- The end of the Template class -------------------
    
    
    TestResult = unittest.TestResult
    
    
    class _TestResult(TestResult):
        # note: _TestResult is a pure representation of results.
        # It lacks the output and reporting ability compares to unittest._TextTestResult.
    
        def __init__(self, verbosity=1):
            TestResult.__init__(self)
            self.stdout0 = None
            self.stderr0 = None
            self.success_count = 0
            self.failure_count = 0
            self.error_count = 0
            self.verbosity = verbosity
    
            # result is a list of result in 4 tuple
            # (
            #   result code (0: success; 1: fail; 2: error),
            #   TestCase object,
            #   Test output (byte string),
            #   stack trace,
            # )
            self.result = []
            self.subtestlist = []
    
        def startTest(self, test):
            TestResult.startTest(self, test)
            # just one buffer for both stdout and stderr
            self.outputBuffer = io.StringIO()
            stdout_redirector.fp = self.outputBuffer
            stderr_redirector.fp = self.outputBuffer
            self.stdout0 = sys.stdout
            self.stderr0 = sys.stderr
            sys.stdout = stdout_redirector
            sys.stderr = stderr_redirector
    
        def complete_output(self):
            """
            Disconnect output redirection and return buffer.
            Safe to call multiple times.
            """
            if self.stdout0:
                sys.stdout = self.stdout0
                sys.stderr = self.stderr0
                self.stdout0 = None
                self.stderr0 = None
            return self.outputBuffer.getvalue()
    
        def stopTest(self, test):
            # Usually one of addSuccess, addError or addFailure would have been called.
            # But there are some path in unittest that would bypass this.
            # We must disconnect stdout in stopTest(), which is guaranteed to be called.
            self.complete_output()
    
        def addSuccess(self, test):
            if test not in self.subtestlist:
                self.success_count += 1
                TestResult.addSuccess(self, test)
                output = self.complete_output()
                self.result.append((0, test, output, ''))
                if self.verbosity > 1:
                    sys.stderr.write('ok ')
                    sys.stderr.write(str(test))
                    sys.stderr.write('\n')
                else:
                    sys.stderr.write('.')
    
        def addError(self, test, err):
            self.error_count += 1
            TestResult.addError(self, test, err)
            _, _exc_str = self.errors[-1]
            output = self.complete_output()
            self.result.append((2, test, output, _exc_str))
            if self.verbosity > 1:
                sys.stderr.write('E  ')
                sys.stderr.write(str(test))
                sys.stderr.write('\n')
            else:
                sys.stderr.write('E')
    
        def addFailure(self, test, err):
            self.failure_count += 1
            TestResult.addFailure(self, test, err)
            _, _exc_str = self.failures[-1]
            output = self.complete_output()
            self.result.append((1, test, output, _exc_str))
            if self.verbosity > 1:
                sys.stderr.write('F  ')
                sys.stderr.write(str(test))
                sys.stderr.write('\n')
            else:
                sys.stderr.write('F')
    
        def addSubTest(self, test, subtest, err):
            if err is not None:
                if getattr(self, 'failfast', False):
                    self.stop()
                if issubclass(err[0], test.failureException):
                    self.failure_count += 1
                    errors = self.failures
                    errors.append((subtest, self._exc_info_to_string(err, subtest)))
                    output = self.complete_output()
                    self.result.append((1, test, output + '\nSubTestCase Failed:\n' + str(subtest),
                                        self._exc_info_to_string(err, subtest)))
                    if self.verbosity > 1:
                        sys.stderr.write('F  ')
                        sys.stderr.write(str(subtest))
                        sys.stderr.write('\n')
                    else:
                        sys.stderr.write('F')
                else:
                    self.error_count += 1
                    errors = self.errors
                    errors.append((subtest, self._exc_info_to_string(err, subtest)))
                    output = self.complete_output()
                    self.result.append(
                        (2, test, output + '\nSubTestCase Error:\n' + str(subtest), self._exc_info_to_string(err, subtest)))
                    if self.verbosity > 1:
                        sys.stderr.write('E  ')
                        sys.stderr.write(str(subtest))
                        sys.stderr.write('\n')
                    else:
                        sys.stderr.write('E')
                self._mirrorOutput = True
            else:
                self.subtestlist.append(subtest)
                self.subtestlist.append(test)
                self.success_count += 1
                output = self.complete_output()
                self.result.append((0, test, output + '\nSubTestCase Pass:\n' + str(subtest), ''))
                if self.verbosity > 1:
                    sys.stderr.write('ok ')
                    sys.stderr.write(str(subtest))
                    sys.stderr.write('\n')
                else:
                    sys.stderr.write('.')
    
    
    class HTMLTestRunner(Template_mixin):
    
        def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
            self.stream = stream
            self.verbosity = verbosity
            if title is None:
                self.title = self.DEFAULT_TITLE
            else:
                self.title = title
            if description is None:
                self.description = self.DEFAULT_DESCRIPTION
            else:
                self.description = description
    
            self.startTime = datetime.datetime.now()
    
        def run(self, test):
            "Run the given test case or test suite."
            result = _TestResult(self.verbosity)
            test(result)
            self.stopTime = datetime.datetime.now()
            self.generateReport(test, result)
            print('\nTime Elapsed: %s' % (self.stopTime - self.startTime), file=sys.stderr)
            return result
    
        def sortResult(self, result_list):
            # unittest does not seems to run in any particular order.
            # Here at least we want to group them together by class.
            rmap = {}
            classes = []
            for n, t, o, e in result_list:
                cls = t.__class__
                if cls not in rmap:
                    rmap[cls] = []
                    classes.append(cls)
                rmap[cls].append((n, t, o, e))
            r = [(cls, rmap[cls]) for cls in classes]
            return r
    
        def getReportAttributes(self, result):
            """
            Return report attributes as a list of (name, value).
            Override this to add custom attributes.
            """
            startTime = str(self.startTime)[:19]
            duration = str(self.stopTime - self.startTime)
            status = []
            if result.success_count: status.append(u'通过 %s' % result.success_count)
            if result.failure_count: status.append(u'失败 %s' % result.failure_count)
            if result.error_count:   status.append(u'错误 %s' % result.error_count)
            if status:
                status = ' '.join(status)
            else:
                status = 'none'
            return [
                (u'开始时间', startTime),
                (u'运行时长', duration),
                (u'状态', status),
            ]
    
        def generateReport(self, test, result):
            report_attrs = self.getReportAttributes(result)
            generator = 'HTMLTestRunner %s' % __version__
            stylesheet = self._generate_stylesheet()
            heading = self._generate_heading(report_attrs)
            report = self._generate_report(result)
            ending = self._generate_ending()
            chart = self._generate_chart(result)
            output = self.HTML_TMPL % dict(
                title=saxutils.escape(self.title),
                generator=generator,
                stylesheet=stylesheet,
                heading=heading,
                report=report,
                ending=ending,
                chart_script=chart
            )
            self.stream.write(output.encode('utf8'))
    
        def _generate_stylesheet(self):
            return self.STYLESHEET_TMPL
    
        def _generate_heading(self, report_attrs):
            a_lines = []
            for name, value in report_attrs:
                line = self.HEADING_ATTRIBUTE_TMPL % dict(
                    name=saxutils.escape(name),
                    value=saxutils.escape(value),
                )
                a_lines.append(line)
            heading = self.HEADING_TMPL % dict(
                title=saxutils.escape(self.title),
                parameters=''.join(a_lines),
                description=saxutils.escape(self.description),
            )
            return heading
    
        def _generate_report(self, result):
            rows = []
            sortedResult = self.sortResult(result.result)
            for cid, (cls, cls_results) in enumerate(sortedResult):
                # subtotal for a class
                np = nf = ne = 0
                for n, t, o, e in cls_results:
                    if n == 0:
                        np += 1
                    elif n == 1:
                        nf += 1
                    else:
                        ne += 1
    
                # format class description
                if cls.__module__ == "__main__":
                    name = cls.__name__
                else:
                    name = "%s.%s" % (cls.__module__, cls.__name__)
                doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""
                desc = doc and '%s: %s' % (name, doc) or name
    
                row = self.REPORT_CLASS_TMPL % dict(
                    style=ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
                    desc=desc,
                    count=np + nf + ne,
                    Pass=np,
                    fail=nf,
                    error=ne,
                    cid='c%s' % (cid + 1),
                )
                rows.append(row)
    
                for tid, (n, t, o, e) in enumerate(cls_results):
                    self._generate_report_test(rows, cid, tid, n, t, o, e)
    
            report = self.REPORT_TMPL % dict(
                test_list=''.join(rows),
                count=str(result.success_count + result.failure_count + result.error_count),
                Pass=str(result.success_count),
                fail=str(result.failure_count),
                error=str(result.error_count),
            )
            return report
    
        def _generate_chart(self, result):
            chart = self.ECHARTS_SCRIPT % dict(
                Pass=str(result.success_count),
                fail=str(result.failure_count),
                error=str(result.error_count),
            )
            return chart
    
        def _generate_report_test(self, rows, cid, tid, n, t, o, e):
            # e.g. 'pt1.1', 'ft1.1', etc
            has_output = bool(o or e)
            tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid + 1, tid + 1)
            name = t.id().split('.')[-1]
            doc = t.shortDescription() or ""
            desc = doc and ('%s: %s' % (name, doc)) or name
            tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
    
            script = self.REPORT_TEST_OUTPUT_TMPL % dict(
                id=tid,
                output=saxutils.escape(o + e),
            )
    
            row = tmpl % dict(
                tid=tid,
                Class=(n == 0 and 'hiddenRow' or 'none'),
                style=(n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none')),
                desc=desc,
                script=script,
                status=self.STATUS[n],
            )
            rows.append(row)
            if not has_output:
                return
    
        def _generate_ending(self):
            return self.ENDING_TMPL
    
    
    ##############################################################################
    # Facilities for running tests from the command line
    ##############################################################################
    
    # Note: Reuse unittest.TestProgram to launch test. In the future we may
    # build our own launcher to support more specific command line
    # parameters like test title, CSS, etc.
    class TestProgram(unittest.TestProgram):
        """
        A variation of the unittest.TestProgram. Please refer to the base
        class for command line parameters.
        """
    
        def runTests(self):
            # Pick HTMLTestRunner as the default test runner.
            # base class's testRunner parameter is not useful because it means
            # we have to instantiate HTMLTestRunner before we know self.verbosity.
            if self.testRunner is None:
                self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
            unittest.TestProgram.runTests(self)
    
    
    main = TestProgram
    
    ##############################################################################
    # Executing this module from the command line
    ##############################################################################
    
    if __name__ == "__main__":
        main(module=None)
    

    九、Python+unittest+requests+HTMLTestRunner完整的接口自动化测试框架搭建_08——调用生成测试报告

    先别急着创建runAll.py文件(所有工作做完,最后我们运行runAll.py文件来执行接口自动化的测试工作并生成测试报告发送报告到相关人邮箱),但是我们在创建此文件前,还缺少点东东。按我的目录结构创建caselist.txt文件,内容如下:

    user/test01case
    #user/test02case
    #user/test03case
    #user/test04case
    #user/test05case
    #shop/test_shop_list
    #shop/test_my_shop
    #shop/test_new_shop

    这个文件的作用是,我们通过这个文件来控制,执行哪些模块下的哪些unittest用例文件。如在实际的项目中:user模块下的test01case.py,店铺shop模块下的我的店铺my_shop,如果本轮无需执行哪些模块的用例的话,就在前面添加#。我们继续往下走,还缺少一个发送邮件的文件。在common下创建configEmail.py文件,内容如下:

    # import os
    # import win32com.client as win32
    # import datetime
    # import readConfig
    # import getpathInfo
    # 
    # 
    # read_conf = readConfig.ReadConfig()
    # subject = read_conf.get_email('subject')#从配置文件中读取,邮件主题
    # app = str(read_conf.get_email('app'))#从配置文件中读取,邮件类型
    # addressee = read_conf.get_email('addressee')#从配置文件中读取,邮件收件人
    # cc = read_conf.get_email('cc')#从配置文件中读取,邮件抄送人
    # mail_path = os.path.join(getpathInfo.get_Path(), 'result', 'report.html')#获取测试报告路径
    # 
    # class send_email():
    #     def outlook(self):
    #         olook = win32.Dispatch("%s.Application" % app)
    #         mail = olook.CreateItem(win32.constants.olMailItem)
    #         mail.To = addressee # 收件人
    #         mail.CC = cc # 抄送
    #         mail.Subject = str(datetime.datetime.now())[0:19]+'%s' %subject#邮件主题
    #         mail.Attachments.Add(mail_path, 1, 1, "myFile")
    #         content = """
    #                     执行测试中……
    #                     测试已完成!!
    #                     生成报告中……
    #                     报告已生成……
    #                     报告已邮件发送!!
    #                     """
    #         mail.Body = content
    #         mail.Send()
    # 
    # 
    # if __name__ == '__main__':# 运营此文件来验证写的send_email是否正确
    #     print(subject)
    #     send_email().outlook()
    #     print("send email ok!!!!!!!!!!")
    
    
    # 两种方式,第一种是用的win32com,因为系统等各方面原因,反馈win32问题较多,建议改成下面的smtplib方式
    import os
    import smtplib
    import base64
    from email.mime.text import MIMEText
    from email.mime.multipart import MIMEMultipart
    
    
    class SendEmail(object):
        def __init__(self, username, passwd, recv, title, content,
                     file=None, ssl=False,
                     email_host='smtp.163.com', port=25, ssl_port=465):
            self.username = username  # 用户名
            self.passwd = passwd  # 密码
            self.recv = recv  # 收件人,多个要传list ['a@qq.com','b@qq.com]
            self.title = title  # 邮件标题
            self.content = content  # 邮件正文
            self.file = file  # 附件路径,如果不在当前目录下,要写绝对路径
            self.email_host = email_host  # smtp服务器地址
            self.port = port  # 普通端口
            self.ssl = ssl  # 是否安全链接
            self.ssl_port = ssl_port  # 安全链接端口
    
        def send_email(self):
            msg = MIMEMultipart()
            # 发送内容的对象
            if self.file:  # 处理附件的
                file_name = os.path.split(self.file)[-1]  # 只取文件名,不取路径
                try:
                    f = open(self.file, 'rb').read()
                except Exception as e:
                    raise Exception('附件打不开!!!!')
                else:
                    att = MIMEText(f, "base64", "utf-8")
                    att["Content-Type"] = 'application/octet-stream'
                    # base64.b64encode(file_name.encode()).decode()
                    new_file_name = '=?utf-8?b?' + base64.b64encode(file_name.encode()).decode() + '?='
                    # 这里是处理文件名为中文名的,必须这么写
                    att["Content-Disposition"] = 'attachment; filename="%s"' % (new_file_name)
                    msg.attach(att)
            msg.attach(MIMEText(self.content))  # 邮件正文的内容
            msg['Subject'] = self.title  # 邮件主题
            msg['From'] = self.username  # 发送者账号
            msg['To'] = ','.join(self.recv)  # 接收者账号列表
            if self.ssl:
                self.smtp = smtplib.SMTP_SSL(self.email_host, port=self.ssl_port)
            else:
                self.smtp = smtplib.SMTP(self.email_host, port=self.port)
            # 发送邮件服务器的对象
            self.smtp.login(self.username, self.passwd)
            try:
                self.smtp.sendmail(self.username, self.recv, msg.as_string())
                pass
            except Exception as e:
                print('出错了。。', e)
            else:
                print('发送成功!')
            self.smtp.quit()
    
    
    if __name__ == '__main__':
    
        m = SendEmail(
            username='@163.com',
            passwd='',
            recv=[''],
            title='',
            content='测试发送邮件',
            file=r'E:\test_record\v2.3.3\测试截图\调整样式.png',
            ssl=True,
        )
        m.send_email()

    运行configEmail.py验证邮件发送是否正确

    邮件已发送成功,我们进入到邮箱中进行查看,一切OK~~不过这我要说明一下,我写的send_email是调用的outlook,如果您的电脑本地是使用的其他邮件服务器的话,这块的代码需要修改为您想使用的邮箱调用代码

    如果遇到发送的多个收件人,但是只有第一个收件人可以收到邮件,或者收件人为空可以参考http://www.361way.com/smtplib-multiple-addresses/5503.html

    继续往下走,这下我们该创建我们的runAll.py文件了

    import os
    import common.HTMLTestRunner as HTMLTestRunner
    import getpathInfo
    import unittest
    import readConfig
    from common.configEmail import SendEmail
    from apscheduler.schedulers.blocking import BlockingScheduler
    import pythoncom
    # import common.Log
    
    send_mail = SendEmail(
            username='@163.com',
            passwd='',
            recv=[''],
            title='',
            content='测试发送邮件',
            file=r'E:\test_record\v2.3.3\测试截图\调整样式.png',
            ssl=True,
        )
    path = getpathInfo.get_Path()
    report_path = os.path.join(path, 'result')
    on_off = readConfig.ReadConfig().get_email('on_off')
    # log = common.Log.logger
    
    class AllTest:#定义一个类AllTest
        def __init__(self):#初始化一些参数和数据
            global resultPath
            resultPath = os.path.join(report_path, "report.html")#result/report.html
            self.caseListFile = os.path.join(path, "caselist.txt")#配置执行哪些测试文件的配置文件路径
            self.caseFile = os.path.join(path, "testCase")#真正的测试断言文件路径
            self.caseList = []
    
        def set_case_list(self):
            """
            读取caselist.txt文件中的用例名称,并添加到caselist元素组
            :return:
            """
            fb = open(self.caseListFile)
            for value in fb.readlines():
                data = str(value)
                if data != '' and not data.startswith("#"):# 如果data非空且不以#开头
                    self.caseList.append(data.replace("\n", ""))#读取每行数据会将换行转换为\n,去掉每行数据中的\n
            fb.close()
    
        def set_case_suite(self):
            """
    
            :return:
            """
            self.set_case_list()#通过set_case_list()拿到caselist元素组
            test_suite = unittest.TestSuite()
            suite_module = []
            for case in self.caseList:#从caselist元素组中循环取出case
                case_name = case.split("/")[-1]#通过split函数来将aaa/bbb分割字符串,-1取后面,0取前面
                print(case_name+".py")#打印出取出来的名称
                #批量加载用例,第一个参数为用例存放路径,第一个参数为路径文件名
                discover = unittest.defaultTestLoader.discover(self.caseFile, pattern=case_name + '.py', top_level_dir=None)
                suite_module.append(discover)#将discover存入suite_module元素组
                print('suite_module:'+str(suite_module))
            if len(suite_module) > 0:#判断suite_module元素组是否存在元素
                for suite in suite_module:#如果存在,循环取出元素组内容,命名为suite
                    for test_name in suite:#从discover中取出test_name,使用addTest添加到测试集
                        test_suite.addTest(test_name)
            else:
                print('else:')
                return None
            return test_suite#返回测试集
    
        def run(self):
            """
            run test
            :return:
            """
            try:
                suit = self.set_case_suite()#调用set_case_suite获取test_suite
                print('try')
                print(str(suit))
                if suit is not None:#判断test_suite是否为空
                    print('if-suit')
                    fp = open(resultPath, 'wb')#打开result/20181108/report.html测试报告文件,如果不存在就创建
                    #调用HTMLTestRunner
                    runner = HTMLTestRunner.HTMLTestRunner(stream=fp, title='Test Report', description='Test Description')
                    runner.run(suit)
                else:
                    print("Have no case to test.")
            except Exception as ex:
                print(str(ex))
                #log.info(str(ex))
    
            finally:
                print("*********TEST END*********")
                #log.info("*********TEST END*********")
                fp.close()
            #判断邮件发送的开关
            if on_off == 'on':
                send_mail.send_email()
            else:
                print("邮件发送开关配置关闭,请打开开关后可正常自动发送测试报告")
    # pythoncom.CoInitialize()
    # scheduler = BlockingScheduler()
    # scheduler.add_job(AllTest().run, 'cron', day_of_week='1-5', hour=14, minute=59)
    # scheduler.start()
    
    if __name__ == '__main__':
        AllTest().run()
    
    
    

    执行runAll.py,进到邮箱中查看发送的测试结果报告,打开查看

    然后继续,我们框架到这里就算基本搭建好了,但是缺少日志的输出,在一些关键的参数调用的地方我们来输出一些日志。从而更方便的来维护和查找问题。

    按目录结构继续在common下创建Log.py,内容如下:

    import os
    import logging
    from logging.handlers import TimedRotatingFileHandler
    import getpathInfo
    
    path = getpathInfo.get_Path()
    log_path = os.path.join(path, 'result')  # 存放log文件的路径
    
    
    class Logger(object):
        def __init__(self, logger_name='logs…'):
            self.logger = logging.getLogger(logger_name)
            logging.root.setLevel(logging.NOTSET)
            self.log_file_name = 'logs'  # 日志文件的名称
            self.backup_count = 5  # 最多存放日志的数量
            # 日志输出级别
            self.console_output_level = 'WARNING'
            self.file_output_level = 'DEBUG'
            # 日志输出格式
            self.formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    
        def get_logger(self):
            """在logger中添加日志句柄并返回,如果logger已有句柄,则直接返回"""
            if not self.logger.handlers:  # 避免重复日志
                console_handler = logging.StreamHandler()
                console_handler.setFormatter(self.formatter)
                console_handler.setLevel(self.console_output_level)
                self.logger.addHandler(console_handler)
    
                # 每天重新创建一个日志文件,最多保留backup_count份
                file_handler = TimedRotatingFileHandler(filename=os.path.join(log_path, self.log_file_name), when='D',
                                                        interval=1, backupCount=self.backup_count, delay=True,
                                                        encoding='utf-8')
                file_handler.setFormatter(self.formatter)
                file_handler.setLevel(self.file_output_level)
                self.logger.addHandler(file_handler)
            return self.logger
    
    
    logger = Logger().get_logger()

    然后我们在需要我们输出日志的地方添加日志:

    我们修改runAll.py文件,在顶部增加import common.Log,然后增加标红框的代码

    让我们再来运行一下runAll.py文件,发现在result下多了一个logs文件,我们打开看一下有没有我们打印的日志

    OK,至此我们的接口自动化测试的框架就搭建完了,后续我们可以将此框架进行进一步优化改造,使用我们真实项目的接口,结合持续集成定时任务等,让这个项目每天定时的来跑啦~~~

    欢迎添加我的微信,相互学习探讨~1305618688,qq交流群:849102042

    2020年9月23追加

    一、、最近有太多人反馈,执行通过后report.html文件中内容为空,这个基本上多数原因是因为用例执行异常报错,导致没有成功执行用例,所以没有生成数据。大家可以运行testCase下的test01Case.py等用例文件,看是不是运行报错了。如果运行成功,再去执行runAll试一下

    展开全文
  • 自动化测试框架

    千次阅读 2018-05-09 15:00:15
    什么是自动化测试框架 自动化测试框架是应用于自动化测试的程序框架,它提供了可重用的自动化测试模块,提供最基础的自动化测试功能,或提供自动化测试执行和管理功能的架构模块。它是由一个或多个自动化测试基础...

    什么是自动化测试框架

            自动化测试框架是应用于自动化测试的程序框架,它提供了可重用的自动化测试模块,提供最基础的自动化测试功能,或提供自动化测试执行和管理功能的架构模块。它是由一个或多个自动化测试基础模块、自动化测试管理模块、自动化测试统计模块等组成的工具集合。

    自动化测试框架的作用

            (1)、能够有效组织和管理测试脚本  
            (2)、进行数据驱动或者关键字驱动的测试。
            (3)、将基础的测试代码进行封装,降低测试脚本编写的复杂性和重复性。
            (4)、提高测试将奥本维护和修改的效率。
            (5)、自动执行测试脚本,并自动发布测试报告,为持续集成的开发方式提供脚本支持。
            (6)、让不具备编程能力的测试工程师开展自动化测试工作。

    常见的几种测试框架

    数据驱动测试框架

    框架说明

           数据驱动测试是测试从数据文件(数据池,ODBC源,cvs文件,Excel文件,DAO对象等)中读取输入和输出数值并载入到捕获的或手工编码的脚本中变量里的一种框架。此框架可以将所有测试数据在自动化测试执行的过程中进行自动加载,动态判断测试结果是否符合预期结果,并自动输出测试报告。这个框架一般用于在一个测试流程中使用多组不同的测试数据,以此来验证被测试系统是否能正常工作。

            测试用例包含在数据文件里而不是在脚本里的方面上,这种框架和表格驱动测试有些相似;脚本只是一种“驱动器”(driver)或传送数据的机制。尽管导航的数据不包含在表结构中,但和表格驱动测试还是不同的。在数据驱动测试里,只有测试数据包含在数据文件中。

            这个框架趋向于减少你为了实现所有测试用例而需要的全部的脚本数量,并且在开发绕开错误的办法(Workaround)和维护方面提供了最好的灵活性。和表格驱动测试非常相似的是,表格驱动测试只需要非常少的代码就可以产生大量的测试用例。

    框架优点

            测试数据单独维护,变更测试数据不需要修改程序,不懂编程语言的测试人员也可以修改用例数据进行测试。

    框架缺点
            仅仅是将测试数据从测试脚本中独立出来,任何被测试程序的变更,都会导致测试程序需要进行修改,工作量是大部分架构里最大的,维护成本非常高。


    关键字驱动测试框架

    框架说明

           关键字驱动测试框架可以理解为高级的数据驱动测试框架。使用被操作的元素对象、操作的方法和操作的数据值作为测试过程中输入的自动化测试框架,简单表示为。item.operation(value)。被操作的元素对象、操作的方法和操作的数据值可以保存在数据数组、数据文件、数据库中作为关键字驱动测试框架的输入。

            关键字驱动测试框架属于更高级的自动化测试框架,可以兼容更多的自动化测试操作类型,大大提高了自动化测试框架的使用灵活性。

    框架优点

            极大的减少了自动化开发工程师维护量,普通测试工程师,可以很好的维护自身负责的模块中涉及的测试case和测试数据。

    框架缺点
            框架的抽象程度比较高,对自动化测试工程师的开发能力比较高


    模块化测试框架

    框架说明
            测试脚本模块化框架需要创建能够代表测试下应用程序(application-under-test)的模块,零件(Section)和函数的小的,独立的脚本。然后用一种分级的方式将这些小脚本组成更大的测试,实现一个特定的测试用例。这种框架最容易精通且掌握的。就在一个部件前面构建一个抽象层以掩藏应用程序其他的部件方面,它是一个很著名的编程策略。它把应用程序从在部件的修改中隔离开来并规定了在应用程序设计中的模块性。为了提高自动化测试套件(test suite)的可维护性和可测量性,测试脚本模块化框架应用了抽象或封装的原则。
    框架优点

            模块化框架是最容易掌握和使用的。在一个组件上方建立一个抽象层使其在余下的应用中隐藏起来,这是众所周知的编程技巧。这样应用同组件中的修改隔离开来,提供了程序设计的模块化特性。模块化测试脚本框架使用这一抽象或者封装的原理来提高自动测试组合的可维护性和可升级性。

    框架缺点

            几乎所有大的变更引起的工作量都由自动化测试开发工程师完成,控件识别和业务逻辑本身属于不同的领域,没有很好进行抽象封装。


    函数库结构框架

    框架说明

            测试库构架框架和测试脚本模块化框架非常相似,有着同样的优势,但是它把测试下的应用程序分成过程和函数,而不是脚本。这种框架要求创建代表测试下应用程序模块,零件和函数的库文件(SQABasic libraries, APIs, DLLs等等)。然后这些库文件被测试用例脚本直接调用。

    框架优点

            被测试系统无论是哪层发生变化,只需要相应的人员进行变更维护即可,完成了控件识别操作和业务逻辑的抽象分离。

    框架缺点

            变更引起的工作量还是附加在自动化测试开发工程师身上。

    混合测试自动化框架

    框架说明

            最常见的已实现的框架是上述技术的组合,抽取它们的优点,剔除其弱点。这种混合的测试自动化框架是发展时间较长且应用项目最多的框架。


    展开全文
  • 2019 Python接口自动化测试框架实战开发(一)

    万次阅读 多人点赞 2019-06-28 15:55:25
    说明:该篇博客是博主一字一码编写的,实属不易,请尊重原创,谢谢大家!...整个项目分为四个部分:接口基础丶接口开发丶Unittest与接口测试结合以及接口自动化框架从设计到开发 接口基础包括:H...

    说明:该篇博客是博主一字一码编写的,实属不易,请尊重原创,谢谢大家!

    项目源码下载

    目录

    一丶叙述

    二丶接口基础知识

    三丶接口测试工具

    四丶Fiddler的使用

    五丶unittest使用

    六丶mock服务入门到实战

    七丶接口自动化框架设计到开发


    一丶叙述

    1.项目介绍

    整个项目分为四个部分:接口基础丶接口开发丶Unittest与接口测试结合以及接口自动化框架从设计到开发

    接口基础包括:HTTP接口 / 常见接口 / 接口工具 / 接口基础知识

    接口开发:通过Django来开发get/post接口

    Unittest与接口测试结合:unittest应用 / 断言 / requests引入 / HTMLTestRunner / case的管理

    接口自动化框架从设计到开发:如何设计框架 / 封装工具类 / 重构基类 / 错误调试 / 结果收集以及处理 / 解决数据依赖 / 结果统计及报告发送

    项目整体思路:通过对接口数据文档的读写操作,来获取文档中case的所有数据,然后通过requests模块来发送请求获取的响应数据,通过返回的响应数据中的某个标志性字段的值来判断是否测试成功或者失败,最后将测试的结果数据写入到测试文档或者是html页面又或者是将结果以邮件的形式发送到指定邮箱,这是整个大框架思路,要完成这一系列自动化的测试框架,则需要有一定的python代码基础,博主这里只是粗略的叙述了思路,有很多地方就不细说了比如数据依赖等就请大家慢慢的阅读吧

    2.测试报告效果预览

    • unittest和HTMLTestRunner结合生成报告(新版本的)

    •  unittest和HTMLTestRunner结合生成报告(经典版本的)

    •  测试报告邮件通知

    二丶接口基础知识

    1.什么是接口

    连接前后端以及移动端,通俗来说就是前端和后端之间的桥梁,比如网站需要去调用银行丶微信及支付宝的接口来完成业务需求

    2.接口的种类

    外部接口和内部接口;内部接口又分为上层服务与下层服务以及同级服务

    3.接口的分类

    请求方式:post丶get丶delete丶put

    4.为什么要做接口测试

    原因:不同端的工作进度肯定是不一致的,那么就需要对最开始开发出来的接口进行测试;对于项目来说缩短项目周期,提高开发效率以及提高系统的健壮性

    5.接口测试流程

    需求讨论——需求评审——场景设计——用例设计——数据准备——执行

    6.为什么要设计测试用例

    • 理清思路,避免侧漏
    • 提高测试效率
    • 跟进测试进度
    • 告诉领导做过
    • 跟进重复重复性工作

    7.用例设计分类

    功能用例测试:测试功能是否正常丶测试功能是否按照接口文档实现

    逻辑用例设计:是否存在依赖业务,例如有些操作是需要用户登录成功的状态下才能进行的操作

    异常测试用例设计:参数异常以及数据异常;参数异常包括关键字参数丶参数为空丶多参数丶少参数丶错误参数,数据异常包括关键字数据丶数据为空丶长度不一致丶错误数据

    安全测试用例设计:cookie丶header丶唯一识别码

    三丶接口测试工具

    1.接口测试工具分类

    • 抓取接口工具

    httpwatch:集成于IE和Firefox浏览器中,在其他浏览器中无法使用,查看数据也比较麻烦

    wireshark:只要是经过电脑的所有请求都会去抓取,导致数据量比较庞大,看数据也比较麻烦

    fiddler:轻量级抓包工具,功能比较全,只会记录http请求不会像wireshark工具记录tcp和udp等请求

    • 测试接口工具:

    loadrunner:不仅仅是性能测试工具,由于该工具几乎都是基于http请求,所以也可以用来测试接口

    fiddler:它除了可以抓包还可以向接口发送各种请求

    soapui:接口和自动化测试工具,功能也比较强大

    jmeter:跟loadrunner一样不仅仅是做性能测试,也可以对接口进行测试

    postman:谷歌旗下的接口测试工具

    四丶Fiddler的使用

    1.抓取不同类型接口数据(http以及https)

    • 查看windows本机的IP

    • 配置fiddler

    • 需要保证要抓取的手机与电脑保持同一网段,博主这里使用逍遥模拟器模拟安卓手机,修改手机网络

    • 在高级选项中设置手动代理IP为windows本机IP地址,端口设置与fiddler抓取端口保持一致

    • 再安卓手机中打开知乎app,抓取知乎app的http服务的数据

    • 现在的移动app都是基于https请求的,所以需要在fiddler中设置https请求

    • 然后在手机端浏览器中访问windows电脑IP+port,进行网络安全证书的下载安装

    • 点击下面一个下载证书

    • 然后设置密码即可

    • 证书安装成功后,重新打开知乎app,则成功抓取https请求的数据

    • 在知乎app中随便对一文章进行评论,抓取该app评论接口

    2.数据模拟以及过滤规则

    • 如下图进行选择要过滤的hosts类型,并在输入框添加要过滤的hosts即可

    • 对知乎上的一篇文章进行回答后,获取https://api.zhihu.com/answers接口,查看发送的post请求数据中的content字段内容也就是博主回答的内容

    • 然后进行数据模拟,也就是点击fiddler软件上的replay对https://api.zhihu.com/answers接口进行post请求数据的而二次发送,由于知乎这边设定对一个问题只能进行一次回答,所以知乎服务器返回的json数据提示我们失败,同时也说明对接口进行二次数据发送成功,只是规则逻辑失败

    3.如何模拟接口响应数据

    • 首先第一步,访问知乎app热榜,在fiddler软件中获取接口查看服务器响应的json格式数据,从服务器返回的json数据看出热榜标题字段名为title_area

    • 然后选择服务器返回的数据类型为TextView,点击.View in Notepad即打开数据记事本,如下图在记事本中找到title_area字段的内容,该字段内容进行了将中文转换为一串字符串

    • 将记事本中的title_area字段的数据修改为this is a test for cdtaogang

    • 点击文件——另存为保存到桌面 

    • 回到fiddler中,左侧选中热榜接口,右侧选中AutoResponder,在此窗口下点击Add Rule将左侧的接口添加进去,在右侧下方导入保存在桌面的zhihu_hot.htm文件,最后点击sava保存

    • 回到知乎app中刷新当前热榜页面,则成功返回修改的热榜标题

    4.使用fiddler进行评论接口测试

    • 对一篇文章进行评论,抓取评论接口,因为get请求的接口测试太简单,所以博主这里选择评论接口即POST请求方式

    • 右击评论接口选择copy复制接口的url地址

    • 右侧选择Composer,将复制的评论接口url粘贴到地址栏,并选择POST请求方式

    • 因为评论接口涉及到用户身份验证也就是登录后才能进行评论的,所以需要将comments接口中request headers请求头中的所有请求数据以及请求数据中的TextView的值进行复制

    请求头数据

    请求体数据

    • 将上面复制的请求头和请求体数据分别粘贴到如下输入框中,点击Execute执行发送,然后在左侧则出现了另一个comments接口数据

    • 查看该comments接口,服务器返回的响应数据中与第一个comments接口一致,说明接口测试成功

    五丶unittest使用

    1.unittest简单使用

    • 在IDE中使用python的环境随便创建个py文件,需要注意的是该py文件的名字不能是test.py,否在运行时会出错,unittest包是python自带的不需要下载安装,代码如下
    # -*- coding: utf-8 -*-
    __author__ = 'cdtaogang'
    __date__ = '2019/6/17 13:10'
    
    import unittest
    
    
    class TestMethod(unittest.TestCase):
    
        @classmethod
        def setUpClass(cls):
            print("Method before class execution")
    
        @classmethod
        def tearDownClass(cls):
            print("Method after class execution")
    
        def setUp(self):
            print("------setUp------")
    
        def tearDown(self):
            print("------tearDown------")
    
        def test_01(self):
            print("First test method")
    
        def test_02(self):
            print("The second test method")
    
    
    if __name__ == '__main__':
        unittest.main()
    • 直接run运行以上代码

    2.unittest和request重构封装

    说明:使用requests模块对接口url地址发送请求,通过unittest测试框架进行case测试

    • 首先博主在逍遥安卓模拟器中下载了一个看书app,通过fiddler对app上的某一接口进行获取,之所以选择对此app进行接口测试,是因为该app的所有接口全是POST请求

    • 在PyCharm下新建工程目录,目录下创建base包,在包下创建一个demo.py文件以及test_method.py文件,用于使用unittest框架来测试以上app接口

    • 在demo.py文件中,使用requests get以及post方法进行了封装,主要是根据传递的参数method来对get以及post方法进行分别调用而已,具体实现如下
    import requests
    
    
    class RunMain:
    	def send_get(self,url,data):
    		res = requests.get(url=url,data=data).json()
    		return res
    		
    	def send_post(self,url,data):
    		res = requests.post(url=url,data=data).json()
    		return res
    
    	def run_main(self,url,method,data=None):
    		res = None
    		if method == 'GET':
    			res = self.send_get(url,data)
    		else:
    			res = self.send_post(url,data)
    		return res
    • 在test_method.py文件中则创建测试类以及test方法,在test方法中调用demo.py中的run_main方法,即使用requests模块向传递的接口url地址和请求方式以及请求体发送对应的请求,这里使用setUp方法则是利用其优先调用而对RunMain类进行实例化
    import unittest
    import json
    import HtmlTestRunner
    from .demo import RunMain
    
    
    class TestMethod(unittest.TestCase):
        def setUp(self):
            self.run = RunMain()
    
        def test_01(self):
            url = 'http://api.ishugui.com/asg/portal/call/265.do'
            data = {
                "sstoken":"eyJleHAiOjE1Njg1MzgyNTczMzUsImlhdCI6MTU2MDc2MjI1NzMzNSwicHAiOiIxMTQwNTQ1Njg5MDYwMDQ0ODAwQHNvaHUuY29tIiwidGsiOiIwZkNYSHpjTUZzR0dFMEswbVdvUVFCNWVCanpXa0hmWiIsInYiOjB9.SDYkT9FpWrBbko6xRrESN74IXJhzkqQLtijKjGiVrqA",
                "gidinf":"x011060802ff0fd40695d68140002799751474c540b3",
                "ppinf":"2|1560762257|1561971857|bG9naW5pZDowOnx1c2VyaWQ6Mjg6MTE0MDU0NTY4OTA2MDA0NDgwMEBzb2h1LmNvbXxzZXJ2aWNldXNlOjMwOjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMHxjcnQ6MTA6MjAxOS0wNi0xN3xlbXQ6MTowfGFwcGlkOjY6MTEwNjA4fHRydXN0OjE6MXxwYXJ0bmVyaWQ6MTowfHJlbGF0aW9uOjA6fHV1aWQ6MTY6czk1YWIwNDk5NjE3YmJhNnx1aWQ6MTY6czk1YWIwNDk5NjE3YmJhNnx1bmlxbmFtZTowOnw",
                "pprdig":"kaKPdU0WwIdzL58CqxNz5pgMyv23P0-Y5GRnd5ufPlXIGzrk7_7TlIK5XFQiuoqAHNqGVXHCVd4cB1DIkR5yFZ_nExnSjIZbBJWYlMkrsiIjDYqWCvedZRLm8sZqS0WqA0FcKXuSn3Z0gVRus9YpEonNz5wyuWdUqxaSmzlzygY",
                "ppsmu":"1|1560762257|1561971857|dXNlcmlkOjI4OjExNDA1NDU2ODkwNjAwNDQ4MDBAc29odS5jb218dWlkOjA6fHV1aWQ6MDo|byWcaoPqy02s2_9GHLhZFAQ6Ov_GazMPFLrq115HiSTBS9Ijr33a55quRq2Mr1_6ZMruKEk-BYFpShUaMtwRYA"
            }
            res1 = self.run.run_main(url, "POST", json.dumps(data))
            print(res1)
    
    
        def test_02(self):
            url = 'http://api.ishugui.com/asg/portal/call/265.do'
            data = {
    
            }
            res2 = self.run.run_main(url, 'POST', data)
    
            print(res2)
    
    
    if __name__ == '__main__':
        unittest.main()
    • 运行test_method模块,查看测试接口,test_02则是错误测试

    3.unittest中assert的使用

    • 首先根据返回的结果字典dict数据中的status状态值来判断测试是否通过或者失败,逻辑很基础就不细说了
    class TestMethod(unittest.TestCase):
        def setUp(self):
            self.run = RunMain()
    
        def test_01(self):
            url = 'http://api.ishugui.com/asg/portal/call/265.do'
            data = {
                "sstoken":"eyJleHAiOjE1Njg1MzgyNTczMzUsImlhdCI6MTU2MDc2MjI1NzMzNSwicHAiOiIxMTQwNTQ1Njg5MDYwMDQ0ODAwQHNvaHUuY29tIiwidGsiOiIwZkNYSHpjTUZzR0dFMEswbVdvUVFCNWVCanpXa0hmWiIsInYiOjB9.SDYkT9FpWrBbko6xRrESN74IXJhzkqQLtijKjGiVrqA",
                "gidinf":"x011060802ff0fd40695d68140002799751474c540b3",
                "ppinf":"2|1560762257|1561971857|bG9naW5pZDowOnx1c2VyaWQ6Mjg6MTE0MDU0NTY4OTA2MDA0NDgwMEBzb2h1LmNvbXxzZXJ2aWNldXNlOjMwOjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMHxjcnQ6MTA6MjAxOS0wNi0xN3xlbXQ6MTowfGFwcGlkOjY6MTEwNjA4fHRydXN0OjE6MXxwYXJ0bmVyaWQ6MTowfHJlbGF0aW9uOjA6fHV1aWQ6MTY6czk1YWIwNDk5NjE3YmJhNnx1aWQ6MTY6czk1YWIwNDk5NjE3YmJhNnx1bmlxbmFtZTowOnw",
                "pprdig":"kaKPdU0WwIdzL58CqxNz5pgMyv23P0-Y5GRnd5ufPlXIGzrk7_7TlIK5XFQiuoqAHNqGVXHCVd4cB1DIkR5yFZ_nExnSjIZbBJWYlMkrsiIjDYqWCvedZRLm8sZqS0WqA0FcKXuSn3Z0gVRus9YpEonNz5wyuWdUqxaSmzlzygY",
                "ppsmu":"1|1560762257|1561971857|dXNlcmlkOjI4OjExNDA1NDU2ODkwNjAwNDQ4MDBAc29odS5jb218dWlkOjA6fHV1aWQ6MDo|byWcaoPqy02s2_9GHLhZFAQ6Ov_GazMPFLrq115HiSTBS9Ijr33a55quRq2Mr1_6ZMruKEk-BYFpShUaMtwRYA"
            }
            res1 = self.run.run_main(url, "POST", json.dumps(data))
            # print(type(res1))
            # print(res1['pub'])
            # print(type(res1['pub']))
            if res1['pub']['status'] == 0:
                print("测试通过")
            else:
                print("测试失败")
            print(res1)
    
    
        def test_02(self):
            url = 'http://api.ishugui.com/asg/portal/call/265.do'
            data = {
    
            }
            res2 = self.run.run_main(url, 'POST', data)
            if res2['pub']['status'] == 0:
                print("测试通过")
            else:
                print("测试失败")
            print(res2)
    
    
    if __name__ == '__main__':
        unittest.main()
    • 运行以上代码,查看结果与预期一样

    • 将if判断代码更换成unittest模块中的assert断言进行判断,这里使用assertEqual方法来判断两个值是否相等,当两个值相等则返回OK,当不相同时返回assertEqual方法msg变量自定义的值
    class TestMethod(unittest.TestCase):
        def setUp(self):
            self.run = RunMain()
    
        def test_01(self):
            url = 'http://api.ishugui.com/asg/portal/call/265.do'
            data = {
                "sstoken":"eyJleHAiOjE1Njg1MzgyNTczMzUsImlhdCI6MTU2MDc2MjI1NzMzNSwicHAiOiIxMTQwNTQ1Njg5MDYwMDQ0ODAwQHNvaHUuY29tIiwidGsiOiIwZkNYSHpjTUZzR0dFMEswbVdvUVFCNWVCanpXa0hmWiIsInYiOjB9.SDYkT9FpWrBbko6xRrESN74IXJhzkqQLtijKjGiVrqA",
                "gidinf":"x011060802ff0fd40695d68140002799751474c540b3",
                "ppinf":"2|1560762257|1561971857|bG9naW5pZDowOnx1c2VyaWQ6Mjg6MTE0MDU0NTY4OTA2MDA0NDgwMEBzb2h1LmNvbXxzZXJ2aWNldXNlOjMwOjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMHxjcnQ6MTA6MjAxOS0wNi0xN3xlbXQ6MTowfGFwcGlkOjY6MTEwNjA4fHRydXN0OjE6MXxwYXJ0bmVyaWQ6MTowfHJlbGF0aW9uOjA6fHV1aWQ6MTY6czk1YWIwNDk5NjE3YmJhNnx1aWQ6MTY6czk1YWIwNDk5NjE3YmJhNnx1bmlxbmFtZTowOnw",
                "pprdig":"kaKPdU0WwIdzL58CqxNz5pgMyv23P0-Y5GRnd5ufPlXIGzrk7_7TlIK5XFQiuoqAHNqGVXHCVd4cB1DIkR5yFZ_nExnSjIZbBJWYlMkrsiIjDYqWCvedZRLm8sZqS0WqA0FcKXuSn3Z0gVRus9YpEonNz5wyuWdUqxaSmzlzygY",
                "ppsmu":"1|1560762257|1561971857|dXNlcmlkOjI4OjExNDA1NDU2ODkwNjAwNDQ4MDBAc29odS5jb218dWlkOjA6fHV1aWQ6MDo|byWcaoPqy02s2_9GHLhZFAQ6Ov_GazMPFLrq115HiSTBS9Ijr33a55quRq2Mr1_6ZMruKEk-BYFpShUaMtwRYA"
            }
            res1 = self.run.run_main(url, "POST", json.dumps(data))
            # print(type(res1))
            # print(res1['pub'])
            # print(type(res1['pub']))
            # if res1['pub']['status'] == 0:
            #     print("测试通过")
            # else:
            #     print("测试失败")
            self.assertEqual(res1['pub']['status'], 0, "测试失败")
            print(res1)
    
    
        def test_02(self):
            url = 'http://api.ishugui.com/asg/portal/call/265.do'
            data = {
    
            }
            res2 = self.run.run_main(url, 'POST', data)
            # if res2['pub']['status'] == 0:
            #     print("测试通过")
            # else:
            #     print("测试失败")
            self.assertEqual(res2['pub']['status'], 0, "测试失败")
            print(res2)
    
    
    if __name__ == '__main__':
        unittest.main()
    • 测试查看结果,断言失败,测试结果如下很清晰

    4.unittest中case的管理及运用

    • 在测试一些接口时,有些接口的返回数据需要在下一个接口进行使用,所以需要定义全局变量,方便每个case都能够得着,当在test_01中定义全局变量userid,然后在test_02中进行打印

    • 在unittest中,是按照字母数字来进行case先后执行顺序的,将test_01改为test_03后,运行代码后,会提示test_02中的userid未定义,原因是程序先去执行了test_02这个case,所以出现该提示是正常的

    • 当在测试代码中有很多case时,我想跳过某个case,则在该case方法上定义unittest的skip方法装饰器,并需要传递此方法名作为实参进行传递

    • 除了在if __name__ == '__main__'中使用unittest.main方法执行所有的case以外,还可以将要测试的case添加到unittest.TestSuite集合中执行想要执行的case,若想要全部都执行则需要一个一个的添加

    5.unittest和HTMLTestRunner结合生成报告(博主这里给大家展现两种)

    第一种:比较新版本的htmltestrunner报告

    • 然后将下载好的whl文件放在你的项目环境的Scripts目录下

    • 最后在Terminal终端或者cmd终端中进入以上目录,执行如下命令即可

    • 安装成功后,即在以下路径中可以找到安装的HTMLTestRunner的包了

    • 在if __name__ == '__main__'中只需要调用HtmlTestRunner模块中的HtmlTestRunner类,向该类传递报告标题参数值即可,其他均默认,需要注意的时启动文件run为当前的py文件,如果是Unittests开头的启动文件,则不会运行if __name__ == '__main__'下的代码,只会执行unittest框架的setUp以及test开头的case代码

    • 运行test_method.py文件,成功在base目录下创建reports目录,并在该目录下生成对应时间的测试报告

    • 打开reports目录下生成的html测试报告,查看测试内容,与预期设定一样,test_02失败test_03成功,说明一下报告中的乱码为中文

    第二种:比较经典版本的htmltestrunner报告

    • 为了方便演示效果,博主在testItems项目目录下,创建base2的模块,将base模块下的demo.py和test_method.py文件拷贝到base2目录下并将test_method.py命令为test_method2.py免得搞混淆,然后在base2目录下新建HTMLTestRunner.py文件用于存放其源码,目录结构如下

    第94行, 将import StringIO修改成import io
    第539行,将self.outputBuffer = StringIO.StringIO()修改成self.outputBuffer = io.StringIO()
    第642行,将if not rmap.has_key(cls):修改成if not cls in rmap:
    第631行,将print >> sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)修改成print(sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime))
    第766行,将uo = o.decode('latin-1')修改成uo = e
    第775行,将ue = e.decode('latin-1')修改成ue = e
    • 在test_method2模块中首先需要从base2模块中去导入HTMLTestRunner文件,然后if __name__ == '__main__'中,需要创建一个文件源,同样是调用HTMLTestRunner模块中的HTMLTestRunner类,不同的是需要将创建的文件源传递给实例属性stream变量

    • 运行test_method2.py,成功在上一级report目录下生成html_report.html报告文件

    • 打开html_report.html测试报告,测试结果与代码设定一致

    六丶mock服务入门到实战

    1.mock简介

    mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法,mock是在测试过程中,对于一些不容易构造/获取的对象,创建一个mock对象来模拟对象的行为即就是模拟fiddler返回接口响应数据的一个过程。

    2.mock安装

    • 在终端使用pip进行安装即可

    3.在case中通过底层函数实现mock

    • 在test_method模块中导入mock,然后在test_03函数中通过以下代码设置返回的return_value的值为请求的data数据
    mock_data = mock.Mock(return_value=data)
    print(mock_data)
    • run运行Unittests in test_method.py,打印出Mock id的值

    • 将调用run_main方法的值设定为mock_data,即print(res1)则表示打印请求的data数据的值,因为res1的数据不再是接口返回的响应数据,则arrest断言是会提示报错的,这是正常的

    4.重构封装mock服务

    • 在base目录下创建mock_demo.py文件,构造一个mock_test方法,该方法就是将test_03方法中self.run.run_main = mock.Mock(return_value=data) 和 res1 = self.run.run_main(url, "POST", json.dumps(data))方法的调用进行了封装成为test_02和test_03方法通用的一个方法,上一步骤中的代码mock_data = mock.Mock(return_value=data) 和self.run.run_main = mock_data,即就相当于self.run.run_main = mock.Mock(return_value=data)而已,都是python基本的调用封装基础知识,mock_demo.py中的代码如下
    # -*- coding: utf-8 -*-
    __author__ = 'cdtaogang'
    __date__ = '2019/6/20 16:26'
    from mock import mock
    import json
    
    def mock_test(mock_method, request_data, url, method, response_data):
        """
        :param mock_method:
        :param request_data:
        :param url:
        :param method:
        :param response_data:
        :return: res
        """
        mock_method = mock.Mock(return_value=response_data)
        print('mock_method:', mock_method)
        res = mock_method(url, method, json.dumps(request_data))
        return res
    • 那么在test_03方法中,如下进行调用即可
    res1 = mock_test(self.run.run_main, data, url, 'POST', 'ssssssss')
    print('res1:', res1)
    • 运行Unittests in test_method.py,查看运行结果和博主设定一样成功返回自定义的response_data数据

    七丶接口自动化框架设计到开发

    1.如何设计一个接口自动化测试框架

    根据接口地址丶接口类型丶请求数据丶预期结果来进行设计,对于需要登录后才能进行操作的接口那么则需要进行header cookie等数据的传递,自动化测试的难点就是数据依赖。

    2.python操作excel获得内容

    • 首先python操作excel,需要安装两个包,分别是xlrd和xlwt这两个库,xlrd这个库是负责读取excel数据的,而xlwt库是负责向excel写入数据的

    • 在项目目录下创建utils工具包,在该包下创建op_excel.py文件,在该文件中通过导入xlrd包,对excel表的数据进行读取操作

    3.重构操作excel函数

    • 根据上一步骤读取excel表的内容代码后,进行了一个简单的封装,提高代码的通用性,过程相当的简单,实现代码如下
    # -*- coding: utf-8 -*-
    __author__ = 'cdtaogang'
    __date__ = '2019/6/20 17:33'
    import xlrd
    
    data = xlrd.open_workbook("../test_data/rs.xls")
    tables = data.sheets()[0]  # 获取表格数据对象
    print(tables.nrows) # 打印表格行数
    print(tables.cell_value(0,0))  # 打印excel表格数据,需要传递数据所在的坐标(x,y)
    print(tables.cell_value(0,1))
    print("*"*50+"封装前后数据对比"+"*"*50)
    
    
    class operationExcel(object):
        def __init__(self, file_path="../test_data/rs.xls", sheet_id=0):
            self.file_path = file_path
            self.sheet_id = sheet_id
            self.data = self.get_data()
    
        def get_data(self):
            data = xlrd.open_workbook(self.file_path)
            tables = data.sheets()[self.sheet_id]
            return tables
    
        def get_rows(self):
            """获取单元格的排数"""
            return self.data.nrows
    
        def get_cell_value(self, x=0, y=0):
            """获取某个单元格的数据"""
            return self.data.cell_value(x, y)
    
    
    if __name__ == '__main__':
        print(operationExcel().get_rows())
        print(operationExcel().get_cell_value())
        print(operationExcel().get_cell_value(0,1))
    • 运行op_excel.py文件后,结果与封装之前代码结果一致,表示重构封装代码成功

    4.学习操作json文件

    • 自定义一个登录的json文件名为login.json,文件内容如下,存放在test_data目录下

    • 在utils工具包下创建op_json.py文件,在文件中对login.json文件内容进行读取操作,代码如下

    5.重构json工具类

    • 将上一步操作json的代码进行封装
    class operationJson(object):
        def __init__(self, file_path="../test_data/login.json"):
            self.file_path = file_path
            self.data = self.get_data()
    
        def get_data(self):
            with open(self.file_path) as f:
                data = json.load(f)
                return data
    
        def get_key_words(self, key=None):
            if key:
                return self.data[key]
            else:
                return self.data
    
    
    if __name__ == '__main__':
        print(operationJson().get_key_words())
        print(operationJson().get_key_words("login"))
        print(operationJson().get_key_words("login")['username'])
    • 运行op_json.py文件,结果与封装之前代码结果一致,表示重构封装代码成功

    6.封装获取常量方法

    • 首先打开excel表格,查看需要获取的字段有哪些

    • 对excel表的字段进行获取,在项目目录下创建名为data的python包,在该包下创建data_conf.py,代码就是简单的获取对应的变量值,具体如下
    # -*- coding: utf-8 -*-
    __author__ = 'cdtaogang'
    __date__ = '2019/6/21 9:29'
    
    
    class global_var:
        id = '0'  # id
        module = '1'  # 模块
        url = '2'  # url
        run = '3'  # 是否运行
        request_type = '4'  # 请求类型
        request_header = '5'  # 是否携带header
        case_depend = '6'  # case依赖
        response_data_depend = '7'  # 依赖的返回数据
        data_depend = '8'  #  数据依赖
        request_data = '9'  # 请求数据
        expect_result = '10'  # 预期结果
        reality_result = '11'  # 实际结果
    
    
    def get_id():
        return global_var.id
    
    def get_module():
        return global_var.module
    
    def get_url():
        return global_var.url
    
    def get_run():
        return global_var.run
    
    def get_request_type():
        return global_var.request_type
    
    def get_request_header():
        return global_var.request_header
    
    def get_case_depend():
        return global_var.case_depend
    
    def get_response_data_depend():
        return global_var.response_data_depend
    
    def get_data_depend():
        return global_var.data_depend
    
    def get_request_data():
        return global_var.request_data
    
    def get_expect_result():
        return global_var.expect_result
    
    def get_reality_result():
        return global_var.reality_result

    7.封装获取接口数据

    • 在data目录下创建data_get.py文件,在该文件中对excel表数据以及json数据结合上一步封装的常量方法整合后的实现,代码如下
    # -*- coding: utf-8 -*-
    __author__ = 'cdtaogang'
    __date__ = '2019/6/21 10:01'
    from utils.op_excel import operationExcel
    from utils.op_json import operationJson
    from data import data_conf
    
    class getData(object):
        def __init__(self):
            self.op_excel = operationExcel()
    
        def get_case_lines(self):
            """获取表格行数"""
            return self.op_excel.get_rows()
    
        def get_is_run(self, x):
            """获取case是否运行"""
            flag = None
            y = data_conf.get_run()
            run_value = self.op_excel.get_cell_value(x, y)
            if run_value == 'yes':
                flag = True
            else:
                flag = False
            return flag
    
        def get_is_header(self, x):
            """是否携带header"""
            y = data_conf.get_request_header()
            header = self.op_excel.get_cell_value(x, y)
            if header == 'yes':
                return data_conf.get_header_value()
            else:
                return None
    
        def get_request_method(self, x):
            """获取请求方式"""
            y = data_conf.get_request_type()
            request_method = self.op_excel.get_cell_value(x, y)
            return request_method
    
        def get_request_url(self, x):
            """获取请求地址"""
            y = data_conf.get_url()
            request_url = self.op_excel.get_cell_value(x, y)
            return request_url
    
        def get_request_data(self, x):
            """获取请求数据"""
            y = data_conf.get_request_data()
            request_data = self.op_excel.get_cell_value(x, y)
            if request_data == '':
                return None
            return request_data
    
        def get_data_for_json(self, x):
            """通过excel中的关键字去获取json数据"""
            op_json = operationJson()
            data = op_json.get_key_words(self.get_request_data(x))
            return data
    
        def get_expect_data(self, x):
            """获取预期结果数据"""
            y = data_conf.get_expect_result()
            expect_data = self.op_excel.get_cell_value(x, y)
            if expect_data == '':
                return None
            return expect_data

    8.post、get基类的封装

    • 在base包下创建run_method.py文件,在文件中重新编写对get丶post请求方式的代码封装,具体如下
    # -*- coding: utf-8 -*-
    __author__ = 'cdtaogang'
    __date__ = '2019/6/21 11:19'
    import requests
    
    
    class RunMain(object):
    
        def get_main(self, url, data=None, header=None):
            res = None
            if header is not None:
                res = requests.get(url=url, data=data, headers=header).json()
            else:
                res = requests.get(url=url, data=data).json()
            return res
    
        def post_main(self, url, data, header=None):
            res = None
            if header is not None:
                res = requests.post(url=url, data=data, headers=header).json()
            else:
                res = requests.post(url=url, data=data).json()
            return res
    
    
        def run_main(self, url, method, data=None, header=None):
            res = None
            if method.lower() == 'post':
                res = self.post_main(url, data, header)
            elif method.lower() == 'get':
                res = self.get_main(url, data, header)
            else:
                return "what ?????"
            return res

    9.主流程封装及错误解决调试

    • 首先在testItems项目目录下新建一个名为main的python包,在该包下创建名为run_test的py文件,该文件为主程序启动文件,代码的逻辑就是将前面封装的方法进行了调用核心就是读取excel表的数据,通过读取到的数据,发送请求,其中包括某一些变量的判断,根据该判断然后到json数据中获取请求的数据,最后就这么的简单,代码如下
    # -*- coding: utf-8 -*-
    __author__ = 'cdtaogang'
    __date__ = '2019/6/21 11:57'
    from base.run_method import RunMain
    from data.data_get import getData
    
    
    class RunTest(object):
        def __init__(self):
            self.runmain = RunMain()
            self.data = getData()
    
        def run(self):
            res = None
            row_counts = self.data.get_case_lines()  # 获取excel表格行数
            # print(row_counts) 5
            for row_count in range(1, row_counts):
                # print(row_count) 1,2,3,4
                url = self.data.get_request_url(row_count)  # y行不变遍历获取x列的请求地址
                method = self.data.get_request_method(row_count)  # y行不变遍历获取x列的请求方式
                is_run = self.data.get_is_run(row_count)  # y行不变遍历获取x列的是否运行
                data = self.data.get_data_for_json(row_count)  # y行不变遍历获取x列的请求数据,这里面时三次调用,依次分别是get_data_for_json丶get_key_words丶get_request_data
                header = self.data.get_is_header(row_count)
                print('url:', url)
                print('method:', method)
                print('is_run:', is_run)
                print('data:', data)
                print('header:', header)
    
                if is_run:
                    res = self.runmain.run_main(url,method,data,header)
                    print("*"*60+"分割线"+"*"*60)
            return res
    
    
    if __name__ == '__main__':
        print('res:', RunTest().run())
    • 运行run_test,成功的将excel以及json数据正确打印出来,返回res服务器返回结果,需要说明的是excel表中的所有数据都不是真实存在的,包括json文档数据也是,这里主要是测试整个框架的正确性读取excel以及json文档数据,并正确的发送请求获得相应数据

    • 运行结果出现红色的内容,是由requests模块发送请求的安全请求警告,如不想显示此警告,可以在run_method.py发送请求核心代码进行禁用,禁用代码如下

    • 重新运行run_test,安全请求警告不再显示

    • 根据代码运行结果,对比excel表以及json数据文档内容,数据正确无误

    10. 返回数据格式处理以及调错

    • 为了测试返回的接口的响应数据,博主这里在excel文档以及json文档中添加了一条数据

    • 因为在excel文档中小说的接口不携带header所以在向接口发送请求数据核心代码块,进行了如下修改,因为在excel文档中的最后一个接口时真实的,所以只需要对最后一个接口url返回的字典类型的响应数据进行转换成json格式的数据,并按照关键字进行排序

    • 运行run_test,在最后一个接口中成功打印出我们想要的数据

    11.获取接口返回状态

    • 在发送请求数据核心代码中,进行打印返回的状态码status_code即可,最后一个接口比较特殊,返回的响应数据中没有status_code,所以需要对返回的json数据中的status进行判断,并向其返回数据中添加我们所要的status_code的值

    • 运行代码,当返回的状态码为404表示接口不存在,只要是存在响应数据,则status_code为200,必须说明一点的就是status_code为200不一定表示接口存在,有些大型网站会对其域名下不存在的接口返回200的错误页面显示,所以在测试文档中会体现预期结果和实际结果两项数据需要一致才能表示测试通过

    12.通过预期结果判断case是否执行成功

    • 进行接下来的测试,博主这里重新准备了另一个excel表来进行测试,需要对json文件中的数据进行添加,在excel表Book-05中的请求数据book5关键字对应的json文件的数据故意为空,可以对测试结果有一个对比

    • 在数据获取核心类中定义了一个方法来获取excel表模块字段的数据

    • 回过头在启动文件中获取模块名预期结果并进行打印

    • 运行启动文件,查看运行结果

    • 在utils目录下,创建common_util.py文件,在该文件代码中通过启动文件传递过来的数据来判断excel表预期结果数据的status状态码与res结果中的status状态码是否一致,一致表示测试通过,不一致则失败,代码如下
    # -*- coding: utf-8 -*-
    __author__ = 'cdtaogang'
    __date__ = '2019/6/21 18:43'
    import json
    
    class CommonUtil(object):
        def is_contains(self, expect, reality):
            flag = None
            reality = json.loads(reality)
            expect = json.loads(expect)
            if expect['status'] == reality['pub']['status']:
                flag = True
            else:
                flag = False
            return flag
    • 在启动文件中需要注释掉res响应数据,利于查看测试结果,还需要在启动文件调用is_contains方法来根据其返回值判断测试是否通过

    • 运行启动文件,查看测试结果

    13.将测试结果写入到excel中

    • 首先在op_excel.py中定义一个方法,该方法实现读取excel的数据,并进行copy复制,然后再write方法将数据写入到坐标位置

    • 然后在data_get.py中需要定义一个方法,在该方法中核心逻辑为获取y坐标的值

    • 最后在启动文件中,调用data_get模块中的write_reality_data方法,并将剩余的x坐标的值以及data数据传递给最终的核心方法write_reality_result_data来完成对excel表中的实际结果数据的写入

    • 将excel表进行关闭后,运行启动文件,再次打开excel表,实际结果数据写入正确,之所以需要关闭excel是避免提示提示错误,无法写入保存数据

     

    展开全文
  • Robot Framework 自动化测试框架

    万人学习 2019-11-29 14:57:20
    Robot Framework 自动化测试框架,包括接口测试、数据库测试、Web测试、App测试。
  • 自动化测试自动化测试框架与工具

    千次阅读 多人点赞 2021-05-06 21:01:01
    文章目录1)什么是自动化测试框架?...1.4 判断是否需要自动化测试适用项目:2)自动化测试框架的类型2.1 自动化测试框架的分类2.1.1 基于模块的测试框架:2.1.2 库架构测试框架2.1.3 数据驱动测试框架2.1.4 关键字驱动



    1)什么是自动化测试框架?

    1.1 什么是框架?

    框架是整个或部分系统的可重用设计,表现为一组抽象构件及构件实例间交互的方法。它规定了应用的体系结构,阐明了整个设计、协作构件之间的依赖关系、责任分配和控制流程,表现为一组抽象类以及其实例之间协作的方法。

    框架一般是成熟、稳健的,它可以处理系统很多细节问题,比如,事物处理,安全性,数据流控制等问题。还有框架一般都经过很多人使用,所以结构很好,所以扩展性也很好,而且它是不断升级的,可以直接享受别人升级代码带来的好处。

    1.2 什么是自动化测试框架?

    自动化测试框架是为自动化测试用例或者脚本提供执行环境而搭建的基础设施。自动化测试框架有助于有效地开发、执行和报告自动化测试用例。

    1.2.1 优点:

    • 代码复用
    • 提高测试效率
    • 更高的测试覆盖率
    • 维护成本低
    • 更早发现和记录bug

    在这里插入图片描述

    1.2.2 框架的基本组件

    1、需要配置文件管理:

    一般需要一个配置文件去控制一些环境信息、开关。配置文件可以是txt/xml/yaml/properties/ini,一般.properties使用较多在JAVA里,Python的话通常会选择ini文件。

    2、业务逻辑代码和测试脚本分离

    如果代码和脚本在一个类文件,那么就根本没有用到代码重构,复用。代码和用例文件分离后,会更加清晰,可以有更多人开发脚本,方便调试。

    3、报告和日志文件输出

    执行了多少case,case结果如何,这都需要报告来展示,一般采用第三方插件来实现这个功能。好多报告格式是html,简单,明了的风格。日志输出也很重要,如果发生报错,脚本执行失败,通过日志快速定位发生问题位置。

    4、自定义的库的封装

    很多功能需要重复调用,可以写成一个公用方法,放到工具包下,每次方便调用,例如浏览器引擎类和basepage.py的封装。

    5、管理、执行脚本方式

    例如Python中单元测试框架unittest使用率非常高。

    6、第三方插件引入

    有时候,一些功能需要借助第三方插件,能够更好实现,例如AutoIT(来实现文件上传和下载)。还有利用第三方报告插件生成基于html格式的测试报告。

    7、持续集成

    git、svn、ant、maven,jenkins,我们会把这整合到jenkins,达到持续集成,一键执行测试脚本。

    总结

    一个简单的自动化测试框架,可能包含哪些组成部分:
    在这里插入图片描述

    1.3 为什么需要自动化测试框架?

    • 使自动化测试的实施更容易:一个好的自动化测试框架是可以让不那么懂技术的人也可以写自动化测试脚本。
    • 解决自动化测试脚本本身存在的问题:如异常处理和场景恢复。
    • 测试易于维护
    • 可重用性:可以实现一些通用功能,简化脚本开发过程。
    • 测试报告美观易读。

    1.4 判断是否需要自动化测试

    自动化测试成本投入高,风险大,并且不是所有的项目都适合自动化。自动化测试的优势在于反复迭代,它的价值产出在于长期的回归测试,以保证被测产品长期稳定地版本更新。

    • 适用项目:

      • 项目周期要长,且需求不会频繁变更。
      • 系统中多数对象要可以被识别,并且不存在大量第三方插件。

    2)自动化测试框架的类型

    2.1 自动化测试框架的分类

    下面是目前流行的各种类型的自动化测试框架。这些框架可能基于对不同关键因素(例如驱动类型、可重用性、易于维护等)进行自动化的支持而彼此不同。

    2.1.1 基于模块的测试框架:

    在这里插入图片描述

    这种框架将整个“测试中的应用程序”分为许多逻辑和独立的模块。对每个模块,创建一个独立的测试脚本,这些脚本结合在一起时,会构建成更大的测试脚本,代表多个模块,这些模块被抽象层隔开,这样在应用程序的各个部分所做的更改不会对该模块产生影响。

    • 缺点:在为每个模块实现测试脚本的同时,测试数据已经嵌入到测试脚本中,这导致使用不同的测试数据进行测试时需要在测试脚本中进行操作。

    2.1.2 库架构测试框架

    在这里插入图片描述
    库体系结构测试框架是建立在基于模块的测试框架之上,但比后者有一些额外的优势。它没有将测试的应用程序划分为测试脚本,而是划分为函数。因此,为测试中的应用程序创建一个由公共函数组成的公共库,当需要时,可以从测试脚本中调用这些库

    它的基本原理就是确定通用的步骤,并将这些步骤分组到公共库下的函数中,在需要的时候在测试脚本中调用这些函数。

    比如,一个登录的步骤,可以把它组合成一个函数,并保存到一个库中,登录时可以直接从库中调用这个函数,而不需要重新再编写代码。

    • 缺点:像基于模块的框架一样,测试数据嵌入测试脚本中,改变数据需要修改脚本;随着越来越多的库的引入,可能会使框架越来越复杂。

    2.1.3 数据驱动测试框架

    在这里插入图片描述
    数据驱动测试框架将测试脚本逻辑和测试数据彼此分离可以把测试数据单独存储起来,存储数据的可以是 xml文件、excel文件、文本文件、csv文件、ODBC数据库等。一般都是以 key-value 格式存储,方便获取使用。

    • 缺点:
      • 过程比较复杂,可能需要定制测试数据源格式和读取机制
      • 只有具有相同执行步骤集的相似测试用例才能组合成多组数据。不同的执行步骤集需要不同的测试用例

    2.1.4 关键字驱动测试框架

    在这里插入图片描述
    关键字驱动测试框架是对数据驱动测试框架的扩展,从某种意义上说,它不仅将测试数据从脚本中分离出来,它还将数据测试脚本的特定代码集保存到外部数据文件中。这些代码集被称为关键字,每个关键字都一种操作。关键字和测试数据都是独立于该框架

    • 缺点:
      • 需要懂得关键字的创建机制,从而可以自己开发关键字
      • 随着越来越多的关键字引入,可能会使框架逐渐变得复杂
      • 测试用例变得更长且复杂,从而影响测试用例的可维护性

    2.1.5 混合测试框架

    在这里插入图片描述
    混合测试框架就是上述(模块化,数据驱动和关键字驱动)多中类型框架的组合。就是利用各种类型框架的优点,组合起来的混合型测试框架。

    在这种框架中,通过将测试用例结合到模块化测试框架中,从模块化脚本中开发测试用例。每个测试用例都使用一个驱动程序脚本,该脚本使用数据驱动框架中的数据文件和关键字驱动框架中的操作文件。

    • 缺点:会比其他类型的测试框架更为复杂一些,例如阅读、维护等

    2.1.6 行为驱动开发框架

    行为驱动测试框架可以让开发人员、测试人员等以易于阅读和理解的格式实现功能验证的自动化。可通过自然语言来描述测试功能,测试场景,测试步骤,测试结果等。


    2.2 流行的自动化测试框架

    2.2.1 Robot Framework

    如果要使用python自动化测试框架进行测试自动化,则Robot Framework是最佳选择。Robot Framework是基于Python的,但是也可以使用 Jython(Java)或IronPython(.NET)语言进行脚本用例的编写。

    • Robot Framework使用关键字驱动的方法使测试用例易于创建。
    • Robot Framework还可以测试MongoDB、FTP、Android、Appium等。
    • 拥有非常多的测试库,包括Selenium WebDriver库和其他常用的的工具
    • Robot Framework具有许多API,可帮助使其尽可能地扩展。

    2.2.2 WebdriverIO

    WebdriverIO是基于Node.js的自动化测试框架。它具有集成的测试运行程序,可以为Web应用程序以及本机移动APP运行自动化测试用例。而且,WebdriverIO可以同时在WebDriver协议和Chrome Devtools协议上运行,从而使其对于基于Selenium Webdriver的跨浏览器测试或基于Chromium的自动化都非常有效。由于WebDriverIO是开源的,因此测试工程师可以获得一堆满足自动化需求的插件。

    2.2.3 Citrus

    Citrus是一个开源测试框架,测试人员可以使用该框架自动化针对任何消息传递协议或数据格式的集成测试。对于诸如REST、HTTP、SOAP或JMS之类的任何消息传递传输,Citrus测试框架都适用于测试消息传递集成。如果需要与用户界面进行交互然后验证后端过程,则可以将Citrus与Selenium进行集成

    2.2.4 Cypress

    Cypress是一个以开发人员为中心的测试自动化框架,该框架使TDD测试驱动开发对开发人员而言成为现实。它的设计原理是能够非常简单地将所有内容打包并捆绑在一起以进行整个端到端测试。Cypress的架构与Selenium不同。Selenium WebDriver在浏览器外部远程运行,而Cypress在其内部运行。这种方法有助于了解浏览器内部和外部发生的所有事情,以提供更一致的测试结果。

    2.2.5 Selenium

    Web应用程序中最受欢迎的开源测试自动化框架之一。Selenium具有跨平台和跨浏览器功能,它还可以用作许多其他测试工具的基础。Selenium支持多种编程语言,例如Java、C#、PHP、Python、Ruby等。它拥有最大的在线社区之一,因此更加易于维护。Selenium可以通过广泛的库和API进行高度扩展,以满足每个团队的需求。Selenium是测试人员首选的测试框架,因为可以编写更高级的测试脚本来满足各种复杂程度。它提供了用于测试创作的回放工具,而无需学习特定的脚本语言。

    2.2.6 Carina

    Carina使用流行的开源解决方案(如Appium、TestNG和Selenium)构建的,可减少对特定技术堆栈的依赖。测试人员可以测试APP(原生APP、混合APP、WebAPP)、WEB应用程序、REST服务和数据库。Carina框架支持不同类型的数据库,例如MySQL、SQL Server、Oracle、PostgreSQL,从而提供了使用MyBatis ORM框架实现DAO层的完美体验。Carina支持所有流行的浏览器和移动设备,并且在IOS/Android之间重用了高达80%的测试自动化代码。API测试基于Freemarker模板引擎,它在生成REST请求方面提供了极大的灵活性。Carina是跨平台的,可以在Unix或Windows OS上轻松运行测试用例。

    3)Selenium

    Selenium是一个用于Web应用程序测试的工具,是一套完整的web应用程序测试系统,包含了测试的录制(selenium IDE)、编写及运行(Selenium Remote Control)和测试的并行处理(Selenium Grid)。Selenium的核心Selenium Core基于JsUnit,完全由JavaScript编写,因此可以用于任何支持JavaScript的浏览器上。Selenium可以模拟真实浏览器,自动化测试工具,支持多种浏览器,爬虫中主要用来解决JavaScript渲染问题。

    这个工具的主要功能包括:测试与浏览器的兼容性,测试你的应用程序看是否能够很好得工作在不同浏览器和操作系统之上。

    • 优点:

      • 开源:不需要钱,且方便进行二次开发,例如提取对json和xml的处理来实现的数据驱动等。
      • 灵活性:因为更加入了xpath(当然大型项目的脚本里xpath 慎用,尽量取id或稳定的属性),加上配合IDE进行定位等,效果比较好。
      • 资料更全面,用该框架的公司越来越多。

    • 缺点:

      • 无论是RC还是Webdriver,对测试人员的编码水平有一定要求。
      • ant,testng,hudson使用也都是小众,大多数人执行这个框架前需要有较长时间学习适应。
      • 时间较短,不如QTP如此完善。

    3.1 selenium 1.0

    selenium 1.0 包括两部分:selenium server、 Client Libraries。

    支持的浏览器包括IE(7, 8, 9, 10, 11),Mozilla Firefox,Safari,Google Chrome,Opera,Edge等。

    3.1.1 selenium server

    selenium server负责控制浏览器的行为。主要有launcher,Http Proxy,selenium core。selenium core使用Selenium Server嵌入到浏览器页面中。实质上,selenium core是由JS函数组成,这样我们可以实现用程序对浏览器进行操作。

    3.1.2 client Libraries

    编写测试用例时控制selenium server的库。

    3.1.3 testcase的执行过程:

    在这里插入图片描述
    1、测试案例(Testcase)通过Client Lib的接口向Selenium Server(一个独立的中间服务器,确切地说是代理服务器)发送Http请求,要求和Selenium Server建立连接

    2、Selenium Server的Launcher启动浏览器,把Selenium Core加载入浏览器页面当中,并把浏览器的代理设置为Selenium Server的Http Proxy。

    3、Testcase通过Client Lib的接口向Selenium Server发送Http请求,Selenium Server对请求进行解析,然后通过Http Proxy发送JS命令通知Selenium Core执行操作浏览器的动作。

    4、Selenium Core接收到指令后,执行操作

    5、浏览器收到新的页面请求信息(因为在4中,Selenium Core的操作可能引发新的页面请求),于是发送Http请求,请求新的页面。由于Selenium Server在启动浏览器时做了手脚,所以Selenium Server会接收到所有由它启动的浏览器发送的请求

    6、Selenium Server接收到浏览器的发送的Http请求后,自己重组Http请求,获取对应的Web页面

    7、Selenium Server的Http Proxy把接收的Web页面返回给浏览器

    3.2 selenium 2.0(Webdriver)

    Selenium 2.0 将浏览器原生的API封装成WebDriver API,可以直接操作浏览器页面里的元素,甚至操作浏览器本身(截屏,窗口大小,启动,关闭,安装插件,配置证书之类的),所以就像真正的用户在操作一样。

    3.2.1 Selenium2的架构:

    在这里插入图片描述

    webdriver按照server–client的经典设计模式设计:

    • server端就是remote server,可以是任意的浏览器:我们的脚本启动浏览器后,该浏览器就是remote server,它的职责就是等待client发送请求并做出相应。
    • client端简单说来就是我们的测试代码:测试代码中的一些行为,比如打开浏览器,转跳到特定的url等操作是以http请求的方式发送给被server端(也就是被测浏览器)server接受请求,并执行相应操作,并在response中返回执行状态、返回值等信息

    3.2.2 WebDriver Wire

    the WebDriver Wire Protocol是Selenium自己设计定义的协议,这套协议非常之强大,几乎可以操作浏览器做任何事情,包括打开、关闭、最大化、最小化、元素定位、元素点击、上传文件等。

    WebDriver Wire协议是通用的,也就是说不管FirefoxDriver还是ChromeDriver,启动之后都会在某一个端口启动基于这套协议的Web Service。例如FirefoxDriver初始化成功,默认从http://localhost:7055开始,IE则是http://localhost:52432。

    3.2.3 webdriver的工作原理:

    1、启动浏览器后,selenium-webdriver会将目标浏览器绑定到特定的端口,启动后的浏览器则作为webdriver的remote server。

    2、客户端(也就是测试脚本),借助ComandExecutor发送HTTP请求给sever端(通信协议:The WebDriver Wire Protocol,在HTTP request的body中,会以WebDriver Wire协议规定的JSON格式的字符串来告诉Selenium我们希望浏览器接下来做什么事情)。

    3、Sever端需要依赖原生的浏览器组件,转化Web Service的命令为浏览器native的调用来完成操作。

    4)Appium

    Appium是一个开源的、跨平台的自动化测试工具,支持IOS、Android和FirefoxOS平台。 通过Appium,开发者无需重新编译app或者做任何调整,就可以测试移动应用,可以使测试代码访问后端API和数据库。它是通过驱动苹果的UIAutomation和Android的UiAutomator框架来实现的双平台支持,同时绑定了Selenium WebDriver用于老的Android平台测试。开发者可以使用WebDriver兼容的任何语言编写测试脚本,如Java, OC, JS, PHP,Python, Ruby, C#,Clojure 和Perl语言。

    4.1 UIAutomation

    UIAutomation是苹果提供的UI自动化测试框架,使用Javascript编写。

    基于UIAutomation有扩展型的工具框架和驱动型的框架:

    • 扩展型框架以JavaScript扩展库方法提供了很多好用js工具。
    • 注入式的框架通常会提供一些Lib或者是Framework,要求测试人员在待测应用的代码工程中导入这些内容,框架可以通过他们完成对app的驱动。
    • 驱动型UI Automation 在自动化测试底层使用了UI Automation库,通过TCP通信的方式驱动UI Automation来完成自动化测试,通过这种方式,编辑脚本的语言不再局限于JavaScript。

    Automation是Apple官方提供的UI自动化测试的解决方法,但接口不够丰富。

    4.2 UiAutomator

    UIAutomator是由谷歌提供的测试框架,它提供了原生Android app和游戏的高级UI测试。这是一个包含API的Java库,用来创建功能性UI测试,还有运行测试的执行引擎。该库自带Android SDK。

    • 优点:它在运行访问不同的进程时,会给JUnit测试案例特权。库由谷歌社区支持和维护。
    • 缺点:仅支持android4.1(API level 16)及以上。 不支持脚本记录。 支持的重点是Java。

    如自动化测试中有跨APP操作,可以结合UiAutomator实现。



    【部分内容参考自】

    • 什么是自动化测试框架:https://blog.csdn.net/m0_52650621/article/details/110195874
    • Python+Selenium框架设计篇之-什么是自动化测试框架:https://www.cnblogs.com/wangyinghao/p/10607475.html
    • 自动化测试为什么需要框架?:https://blog.csdn.net/kami_ochin_akane/article/details/109480295
    • 自动化测试框架总结:https://www.jianshu.com/p/7239eff75e68
    • 关于自动化测试框架:https://www.cnblogs.com/delav/p/12707396.html
    展开全文
  • 自动化测试架构代码托管在github上,读者可以自行下载 https://github.com/626626cdllp/Test/tree/master/Test_framework 自动化测试框架 在这个自动化测试框架中。 在config目录中存放的是测试配置相关的文件,...
  • 在做Android自动化测试框架开发之前,我们应该先了解目前的发展状况,知道我们的目标是什么,也要了解当前的主流框架,做到知己知彼,取长补短。 首先要思考和了解几个问题: 我们开发自动化框架的目标是什么?如何...
  • LazyAndroid是为了解决安卓UI自动化测试中存在的测试工具学习成本高、测试编码中花费在元素查找中的时间过长等问题而诞生的一款UI自动化测试框架。它基于appium,封装了appiumDriver的设置、安卓基本控件的使用和...
  • 自动化测试框架概念

    千次阅读 2016-07-20 16:32:25
    1、自动化测试框架的产生 2、自动化测试框架常见的4种模式: 3、自动化测试的作用 4、自动化测试框架的设计核心思想
  • 5种比较常见的自动化测试框架

    千次阅读 2020-10-26 14:34:43
    自动化测试框架
  • 自动化测试领域内流传着一个说法:单元测试才是自动化测试的核心,在自动化测试里,无论框架何等完美都不可能脱离单元测试,单元测试将会是自动化测试里最小的单位,把它看作单位一,若干个单位一组成了一个整体,...
  • python pytest selenium 自动化测试框架搭建

    千次阅读 多人点赞 2020-07-01 15:39:46
    python pytest selenium 自动化测试框架搭建 公司一直有这个自动化测试需求,前期利用c++或者python进行了一些自动化脚本的编写。这几天没有版本更迭,基于前期的工作,把自动化测试整理了一部分功能模块。现在的...
  • Python自动化测试框架

    千次阅读 2019-10-27 14:02:15
    Python自动化测试框架 1 什么自动化测试框架 什么是工具——工具(Tools)类似框架,只是工具屏蔽了底层的代码,提供单独的操作界面,以此供用户操作,比如:Selenium IDE、Postman、QTP、Jmeter、LoadRunner等自动...
  • 基于webdriver的web UI自动化测试框架(系统架构+测试脚本应用架构自动化测试系统及应用架构
  • 此篇文章是,如何从零搭建一个属于自己项目的自动化测试框架(第一篇到第十一篇)文章的总录。 思考:我们需要一个什么样的框架 既然要搭一个框架,我们首先得弄明白我们需要一个什么样的框架,这个框架要支持...
  • 基于Python的HTTP接口自动化测试框架实现

    万次阅读 多人点赞 2011-04-22 14:36:00
    基于Python的HTTP接口自动化测试框架实现 作者:张元礼 http://blog.csdn.net/vincetest 一、测试需求描述 对服务后台一系列的http接口功能测试。 输入:根据接口描述构造不同的参数输入值 输出:XML文件 eg:...
  • 自动化测试架构

    千次阅读 2018-12-11 16:34:10
  • 自动化测试架构设计

    2016-07-21 13:56:40
    常见自动化测试框架: 数据驱动 data-driven testing framework 测试脚本模块化 test script modularity framework 测试库构架 test library framework 关键字驱动 keyword-driven testing framework项目自动化...
  • 自动化测试框架思路

    千次阅读 2016-08-14 14:24:20
    第一次写类似的文章,语言组织能力不够,就从最简单的开始一路白话文把自己搭框架时的想法一路写下来,可能比较啰嗦,...所以自动化测试框架的定义为:由一个或多个自动化测试基础模块、自动化测试管理模块、自动化测试
  • 基于Espresso和Dagger的自动化测试框架  测试框架可以使用Android推荐的Espresso. 模拟数据可以使用Dagger2, 一种依赖注入框架.Dagger2没有使用反射, 而是使用预生成代码, 提高执行速度.  基于Espresso和Dagger的...
  • 自动化 测试 框架 开源 测试自动化框架是一组最佳实践,通用工具和库,可帮助质量保证测试... 尽管团队可以构建复杂的自动化测试框架,但是当他们可以使用现有的开源工具,库和测试框架获得相同甚至更好的结果时,通...
  • 基于python的自动化测试框架搭建

    万次阅读 多人点赞 2017-12-04 17:35:10
    自己动手搭自动化测试框架
  • 自动化测试框架结构图

    千次阅读 2017-05-03 22:26:28
    自动化测试框架组织结构图
  • cafe是百度开源的一套自动化测试框架,最近开始着手研究了一下,以下是一些总结,比较基础,就是说说环境搭建。 cafe官网地址:https://baiduqa.github.io/Cafe/ 源码在gitHub上有,...
  • JSON Wire协议是一个抽象规范,定义了用户在自动化脚本里操作该如何映射到Selenium或HTTP请求和响应中,通过JSON Wire协议可以在HTTP服务器之间传输信息,Language Bindings和WebDriver就是通过HTTP协议传输JSON数据...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 540,460
精华内容 216,184
关键字:

自动化测试架构