精华内容
下载资源
问答
  • 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是避免提示提示错误,无法写入保存数据

     

    展开全文
  • 我们开发自动化框架的目标是什么?如何得到想要的框架? 目前主流自动化框架的优劣势分析 剖析主流自动化框架的源码 如何利用框架进行二次开发? 本篇文章介绍上述第一个问题:一:Android自动化测试框架目标 稳定性...

    在做Android自动化测试框架开发之前,我们应该先了解目前的发展状况,知道我们的目标是什么,也要了解当前的主流框架,做到知己知彼,取长补短。
    首先要思考和了解几个问题:

    • 我们开发自动化框架的目标是什么?如何得到想要的框架?
    • 目前主流自动化框架的优劣势分析
    • 剖析主流自动化框架的源码
    • 如何利用框架进行二次开发?

    本篇文章介绍上述第一个问题:

    一:Android自动化测试框架目标

    1. 稳定性好:尽可能使用开发人员对控件定义的id等唯一定位信息,减少通过控件index进行节点判断
    2. 可移植性好:尽可能减少通过坐标点进行控件的判断和事件的记录,而monkey和monkeyRunner都是通过坐标信息
    3. 支持跨应用或跨进程交互:测试框架需支持跨应用,而Instrumentation不支持跨应用
    4. 运行效率高:窗口界面跳转监控和窗口定位要准确,而monkey和monkeyRunner不支持此功能
    5. 测试人员易上手:框架容易理解和掌握,测试脚本易编辑,Instrumentation等框架对测试人员有技术要求
    6. 方便调试:框架足够开放,方便调试
    7. 开发效率高:对APP的事件类型封装程度高,覆盖率高
    8. 控件易捕获:对控件识别性高,稳定性强

    二:如何得到自己想要的框架

    1. 匹配框架:对目标应用程序(App)进行分析,找到特征点与当前的测试框架进行匹配,找到最合适的
    2. 框架结合搭配:针对大型复杂的应用程序,可以通过对不同框架的搭配结合使用来弥补对方的不足,实现较高的覆盖率和稳定性(例:UIAutomator和Robotium融合,覆盖更多事件,支持跨进程)
    3. 框架二次开发,封装:对于某些框架可进行二次开发和封装,提高利用率和稳定性
    4. 自己开发工具:基于现有框架原理,开发相应的测试工具

    在进行后两项的实践之前,我们必须要对当前测试框架进行深入的了解,下篇文章将对当前的主流测试框架进行介绍和分析。

    展开全文
  • 分享我的第一次Selenium自动化测试框架开发过程

    万次阅读 多人点赞 2017-08-19 14:08:40
    由于公司的开发团队偏向于使用Java技术,而且公司倡导学习开源技术,所以我选择用Java语言来进行Selenium WebDriver的自动化框架开发。由于本人没有Java开发经验,以前虽然学过QTP但从没有接触过Selenium,正好通过...

    由于公司的开发团队偏向于使用Java技术,而且公司倡导学习开源技术,所以我选择用Java语言来进行Selenium WebDriver的自动化框架开发。由于本人没有Java开发经验,以前虽然学过QTP但从没有接触过Selenium,正好通过这个机会能学习一下自动化测试,同时也学习一下基本的Java开发过程。

    一、首先是搭建框架开发环境

    按照网上的方法部署eclipse,建立TestAction工程,并Import引用JDK和Selenium-2.44完整包

    二、继续引用和安装相关jar包

    1、首先是要满足数据驱动(场景用例和动作用例、数据用例都需要放到excel表上),就需要引用jxl.rar包(实现调用和操作excel);

    2、需要实现自动化框架(有测试套件、测试层)就需要通过eclipse安装TestNg(网上有相关教程);

    三、构建框架的样例代码

    1、实现能够对excel用例数据的调用(通过jxl的引用),创建ExcelData.java类文件(专门用于对excel的调用),以下截取部分代码样例:

         /**
         * @param fileName   excel文件名
         * @param caseName   sheet名
         */
        public ExcelData(String fileName, String caseName) {
            super();
            this.fileName = fileName;
            this.caseName = caseName;
        }
         /**
         * 获得excel表中的数据
         */
        public Object[][] getExcelData() throws BiffException, IOException {
    
            workbook = Workbook.getWorkbook(new File(getPath()));
            sheet = workbook.getSheet(caseName);
            rows = sheet.getRows();
            columns = sheet.getColumns();
            // 为了返回值是Object[][],定义一个多行单列的二维数组
            @SuppressWarnings("unchecked")
    		HashMap<String, String>[][] arrmap = new HashMap[rows - 1][1];
            // 对数组中所有元素hashmap进行初始化
            if (rows > 1) {
                for (int i = 0; i < rows - 1; i++) {
                    arrmap[i][0] = new HashMap<String, String>();
                }
            } else {
                System.out.println("excel中没有数据");
            }
    
            // 获得首行的列名,作为hashmap的key值
            for (int c = 0; c < columns; c++) {
                String cellvalue = sheet.getCell(c, 0).getContents();
                arrkey.add(cellvalue);
            }
            // 遍历所有的单元格的值添加到hashmap中
            for (int r = 1; r < rows; r++) {
                for (int c = 0; c < columns; c++) {
                    String cellvalue = sheet.getCell(c, r).getContents();
                    arrmap[r - 1][0].put(arrkey.get(c), cellvalue);
                }
            }
            return arrmap;
        }
    
        /**
         * 获得excel文件的路径
         * @return
         * @throws IOException
         */
        public String getPath() throws IOException {
            File directory = new File(".");
            sourceFile = directory.getCanonicalPath() + "\\src\\source\\"
                    + fileName + ".xls";
            return sourceFile;
        }

    2、实现对浏览器的调用,考虑到兼容性,需要同时满足对Chrome、FireFox、IE三大浏览器的调用,我们需要准备相关驱动chromedriver.exe、IEDriverServer.exe,这两驱动都是谷歌和IE官方提供的,可以从网上下载到;而FireFox不需要下载驱动,只要安装浏览器就可调用(Selenium和FireFox属于一个团队开发出来的,待遇就是不一样)。

    有了浏览器驱动后(我们把驱动放到工程目录的WebDriver文件夹下,方便按相对路径统一调用),我们就需要一个能调用浏览器的类,以下提供核心代码样例:

    	public static WebDriver getChromeDriver(String url) {
            //加载Google驱动
    	    //System.setProperty("webdriver.chrome.driver","D:\\java\\chromedriver.exe");
    	    System.setProperty("webdriver.chrome.driver",System.getProperties().getProperty("user.dir")+"\\WebDriver\\chromedriver.exe");
    	    ChromeOptions options = new ChromeOptions();
    	    //通过配置参数禁止data;的出现
    	    options.addArguments("--user-data-dir="+System.getProperties().getProperty("user.home")+"/AppData/Local/Google/Chrome/User Data/Default");
    	     //通过配置参数删除“您使用的是不受支持的命令行标记:--ignore-certificate-errors。稳定性和安全性会有所下降。”提示
    	    options.addArguments("--start-maximized","allow-running-insecure-content", "--test-type");
    	    WebDriver driver = new ChromeDriver(options);
    	    driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);
    	    driver.navigate().to(url);
    	    return driver;
    	}
    	public static WebDriver getFireFoxDriver(String url){
    		System.setProperty("webdriver.firefox.bin", "D:\\Program Files\\Mozilla Firefox\\firefox.exe");
    		// TODO Auto-generated method stub
    		WebDriver driver = new FirefoxDriver();
    	    //Puts a Implicit wait, Will wait for 10 seconds before throwing exception
    		driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
    	    //Launch website
    		driver.navigate().to(url);
    		return driver;
    	}
    	public static WebDriver getIEDriver(String url){
    		//System.setProperty("webdriver.ie.driver", "D:\\java\\IE64\\IEDriverServer.exe");
    		System.setProperty("webdriver.ie.driver", System.getProperties().getProperty("user.dir")+"\\WebDriver\\IE32\\IEDriverServer.exe");
    		DesiredCapabilities capabilities = DesiredCapabilities.internetExplorer();
            capabilities.setCapability(InternetExplorerDriver.INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS,true);
            capabilities.setPlatform(Platform.WINDOWS);
            capabilities.setCapability("silent", true);
    		// TODO Auto-generated method stub
    		WebDriver driver = new InternetExplorerDriver(capabilities);
    	    //Puts a Implicit wait, Will wait for 10 seconds before throwing exception
    		driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
    	    //Launch website
    		driver.navigate().to(url);
    		return driver;
    	}

    3、写一个以数据驱动的场景类,来进行单个事务的用例跑测

    (1)首行我们需要用TesgNg提供的数据驱动方法(@DataProvider),来获取一个场景的用例表数据,这个场景从excel的第一个附表获取

    通过action名,调取用例表(用例表是以action名命名的附表),用例表如下所示(ExpectedObject表示用例校验对象的页面Element标签,用;分隔,分号前面的表示ID,分号后面的表示xpath):

    以下为用例表数据获取的代码:

    @DataProvider(name="action")
        public Object[][] Numbers() throws BiffException, IOException{
    		getActionString = actionData.getActionStr(1);//获取第一个场景的broswer、url、action名
    		ExcelData e=new ExcelData("testdata", getActionString.get(2));
            return e.getExcelData();
        }

    然后通过Java的反射机制,实现动态的获取具体事务类和执行相关操作(每个事务的类名和方法名都与action场景名一致),以下截选相关场景的部分调用代码:

        @Test(dataProvider="action")
        public void testAction(HashMap<String, String> data) throws BiffException, IOException {
            try {
            	Class<?> MyClass = Class.forName(packageName+"."+getActionString.get(2));
            	Method method = MyClass.getMethod(getActionString.get(2),WebDriver.class);
            	@SuppressWarnings("unused")
    					String [] results = (String []) method.invoke(null,driver);
        			String ExpObject=data.get("ExpectedObject");
            	String ExpObject_by=ExpObject.split(";")[0].toString();
            	String ExpObject_Desc=ExpObject.split(";")[1].toString();
            	if(ExpObject_by.length()>0){
            		Assert.assertEquals(driver.findElement(By.id(ExpObject_by)).getText(),data.get("ExpectedData"), getActionString.get(2)+data.get("ID")+"验证结果:");
            	}
            	else if(ExpObject_Desc.length()>0){
            		Assert.assertEquals(driver.findElement(By.xpath(ExpObject_Desc)).getText(),data.get("ExpectedData"), getActionString.get(2)+data.get("ID")+"验证结果:");
            	}
            	
        		WebDriverDemo.WebSleep(500);	
    			} catch (Exception e) {
    				// TODO Auto-generated catch block
    				e.printStackTrace();
    			}
        }

    另外说明的是,调用浏览器的方法,需要明确是放在@BeforeMethod中,还是在@BeforeClass中,如果是登录校验测试,就要保证每次执行测试方法都要打开一次浏览器和关闭一次浏览器,那么我们就要把调用浏览器,和关闭浏览器的方法放到@BeforeMethod中和@AfterMethod中。其他业务测试,只要在一个套件类中打开一次浏览器和关闭一次浏览器就可以,所以用到的是@BeforeClass和@AfterClass。

    4、我们需要再写一个以动作(关键词)驱动的场景类

    同样,调用第二个场景的用例表,样例代码如下:

    	@DataProvider(name="action")
        public Object[][] Numbers() throws BiffException, IOException{	
    		getActionString = actionData.getActionStr(2);//获取第二个场景的broswer、url、action名
    		ExcelData e=new ExcelData("testdata", getActionString.get(2));
            return e.getExcelData();
        }

    然后在测试方法中,动态的调用具体操作动作,获取WebElement标签的方法,包括通过By ID或者By xpath,操作动作以最常见的两个为例(sendKeys、click),以下为样例代码节选:

        @Test(dataProvider="action")
        public void testAction(HashMap<String, String> data) throws BiffException, IOException { 
        	//driver.manage().timeouts().implicitlyWait(5,TimeUnit.SECONDS);//找不到element就再给5秒查找
            try {
            	WebElement TestWebElement = null;
            	String SetObject=data.get("SetObject").trim();
            	String SetObject_by=SetObject.split(";")[0].toString();
            	String SetObject_Desc=SetObject.split(";")[1].toString();
            	if(SetObject_by.length()>0){
            		TestWebElement=driver.findElement(By.id(SetObject_by));
            	}
            	else if(SetObject_Desc.length()>0){
            		TestWebElement=driver.findElement(By.xpath(SetObject_Desc));
            	}
            	if(data.get("SetOperate").equals("sendKeys")){
            		TestWebElement.clear(); 
            		TestWebElement.sendKeys(data.get("SetValue"));
            	}else if(data.get("SetOperate").equals("click")){
            		TestWebElement.click();
            	}
        		String ExpObject=data.get("ExpectedObject").trim();
            	if(ExpObject.length()>0){
            		String ExpObject_by=ExpObject.split(";")[0].toString();
                	String ExpObject_Desc=ExpObject.split(";")[1].toString();
            		if(ExpObject_by.length()>0){
                		Assert.assertEquals(driver.findElement(By.id(ExpObject_by)).getText(),data.get("ExpectedData"), getActionString.get(2)+data.get("ID")+"验证结果:");
                	}
                	else if(ExpObject_Desc.length()>0){
                		Assert.assertEquals(driver.findElement(By.xpath(ExpObject_Desc)).getText(),data.get("ExpectedData"), getActionString.get(2)+data.get("ID")+"验证结果:");
                	}
            	}
            	
        		WebDriverDemo.WebSleep(500);	
    			} catch (Exception e) {
    			// TODO Auto-generated catch block
    				e.printStackTrace();
    			}
        }

    这段方法所调用的用例表如下所示(以登录为例):


    5、剩下就是业务扩展类了,所有复杂的事务都可以单独建立测试类和方法(方便扩展维护,只需要在excel场景表中定义后就能调用,利用的是Java反射机制),在这里就不举例了。
    四、实现测试套件调用和报告输出

    有了以上步骤,一个可扩展的自动化框架已经基本形成,但是还达不到大规模应用测试和脚本方便可移植,这时候我们引入Ant(可以在Eclipse中安装插件,可以直接上网下载后引用),为了能输出漂亮一点的报告格式,我们还引入一个saxon-8.7.jar。

    有了Ant后,我们就可以建议build.xml文件,就能一键bulid我们以上的自动化代码,并将执行测试后的结果输出成报告。

    1、首先我们需要编辑好测试套件调用的testng.xml,简单举例如下:

    <?xml version="1.0" encoding="UTF-8"?> 
    <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"> 
    <suite name="Suite" parallel="false"> 
      <test verbose="2" name="Test_Action"> 
      	<!--<groups> 
          <run> 
          	<include name="aaa" />
             <include name="bbb" />  
             <include name="ccc" /> 
           </run> 
        </groups>-->
        <classes> 
          <class name="TestBrowser.ExcActions"/>
          <class name="TestBrowser.ExcActions2"/> 
        </classes> 
      </test> <!-- Default test --> 
    </suite> <!-- Default suite -->

    2、然后我们需要编辑好一个能引用基础jar包、build测试代码、调用testng、输出漂亮报告的build.xml文件

    <?xml version="1.0" encoding="UTF-8"?>
    
    <project name= "TestAction" basedir= "." default="testoutput"><!--default设置为run表示只执行脚本,设为testoutput表示执行完脚本并输出视图报告-->
    <echo message="import libs" />
    <property name= "lib.dir" value= "lib" /> <!--<property name="libdir" location="${basedir}/lib" />-->
    <!--<property name="testng.output.dir" location="${basedir}/test-output" />-->
    <path id= "test.classpath" >
    
    <!-- adding the saxon jar to your classpath -->
    
    <fileset dir= "${lib.dir}" includes= "*.jar" />
     <fileset dir="${basedir}/selenium-2.44.0">
        <include name="selenium-java-2.44.0.jar" />
        <include name="libs/*.jar" />
     </fileset>
    
    </path>
    <taskdef name="testng" classname="org.testng.TestNGAntTask" classpathref="test.classpath" />
    <target name="clean">
    	<delete dir="build"/>
    </target>
    <target name="compile" depends="clean">
    <echo message="mkdir"/>
     <mkdir dir="build/classes"/>
     <javac srcdir="src" destdir="build/classes" debug="on" encoding="UTF-8" includeAntRuntime="false">
    	<classpath refid="test.classpath"/>
    	</javac>
    </target>
    <path id="runpath"> 
    	<path refid="test.classpath"/> 
    	<pathelement location="build/classes"/> 
    	</path> 
    <target name="run" depends="compile">
    	<testng  classpathref="runpath"  outputDir="test-output">
    		<xmlfileset dir="${basedir}" includes="testng.xml"/>
    		<jvmarg value="-ea" />
    	</testng>
    </target>
    <target name= "testoutput" depends="run">
    <xslt in= "test-output/testng-results.xml" style= "test-output/testng-results.xsl"
    out= "test-output/index1.html" >
    <!-- you need to specify the directory here again -->
    <param name= "testNgXslt.outputDir" expression= "${basedir}/test-output/" />
    	<param name="testNgXslt.showRuntimeTotals" expression="true" />
    <classpath refid= "test.classpath" />
    </xslt>
    </target>
    
    </project>

    3、完成这些后,我们就可以通过Eclipse直接Run As Ant Build我们的自动化脚本了,输出一份还算漂亮的报告:

    同时,需要在事务操作类中,对实际结果和预期结果进行比较,并将测试结果写入excel的用例表中,如下:

    		String[] result=new String [2];
    		result[0] = driver.findElement(By.xpath(pars.get(3).split(";")[1].toString())).getText();
    		result[1] = pars.get(4);
    		if(result[0].equals(result[1])){//pars.size()-1
    			ActionsDemo.modifyExcel(Thread.currentThread().getStackTrace()[1].getMethodName(),k,5,"通过");
    		} else {
    			ActionsDemo.modifyExcel(Thread.currentThread().getStackTrace()[1].getMethodName(),k,5,"失败");
    		}

    五、实现自动化框架脚本的迁移调用

    以上的脚本始终是在Eclipse下编译和调用的,如果要实现灵活迁移,随便换任何一台只装了JDK的电脑都能运行,那么我们就要来点改造

    1、首行是保证我们写的代码中,所以需要引用文件的地方,都用相对路径的方式,避免代码包迁移后需要改路径。

    2、通过批处理调用build文件及用例文件,调用时也是通过批处理自动找到相关路径,避免用绝对路径。

    3、需要用环境变量的地方,尽量用批处理的方式实现,甚至最好是不用配置环境变量,直接调用相引用相对命令文件的路径调用

    以下举个通过bat批处理调用Ant来执行整个框架代码的build:

    @echo off
    ::先将测试用例文件拷到用户目录下
    copy src\source\testdata.xls %UserProfile%\src\source
    %cd%\org.apache.ant_1.9.6\bin\ant.bat -buildfile build.xml
    echo 在%cd%\test-output下查看测试报告
    pause

    六、进一步实现自动化的持续集成

    在以上基础上,我们还可以通过jenkins实现对自动化脚本的调用,以及达到每日构建,持续集成开发的要求。

    1、首先部署jenkins(网上有相关方法),由于本人公司一直在用jenkins,我就省了搭建部署这一步,直接将以上的自动化框架脚本上传

    2、自动化脚本完整目录(包括代码、用例、lib、引用的jar、build.xml文件等)上传到SVN(再自动从SVN下到jenkins所在服务器)

    3、在jenkins中新建一个测试项目TestAction,主要配置如下:

    4、配置完后,就可以立即构建(如果碰到相关报错问题,就按输出的提示进行处理),构建成功后,就可以在HTML_Report中看到测试结果:

    七、后续处理

    到此为止,一个完整的Selenium自动化框架就出来了,要说好用不,不好说,还得经过实践的检验,但是以上这个思考过程和框架的演进过程,应该也是值得借鉴的,毕竟这是我这几天摸索和学习的过程,对于一个没有从事过自动化测试,而且没有做过Java开发的测试人员来说,这只是个开始。

    目前来看,这个框架在架构分层上,还是不够清晰,有很多要改进的东西,从技术上来说,我已经实现了我的目标(学习自动化测试),但是在整体架构和代码重构上,还有很多工作没做,以下贴出一份Selenium自动化框架的分层结构,以便后期按照这个标准进行改进:

    测试数据层:独立封装数据;
    页面对象层:封装页面对象,共页面任务层做调用;
    页面任务层:实现各个独立页面的操作;
    测试层:实现页面测试;
    测试套件层:实现测试层的管理调用;

    展开全文
  • 完整的框架源码下载...一、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试一下

    展开全文
  • Appium自动化框架简介

    千次阅读 2018-03-27 14:22:22
    Appium Appium简介 Appium结构流程 Appium工作原理 Appium架构分析 Appium简介 ... 1.... 2.... 3.一个自动化框架不需要重复造轮子 4.一个自动化框架需要开源,在精神和实践上实现开源 appium扩...
  • 说明:该篇博客是博主一字一码编写的,实属不易...七丶接口自动化框架设计到开发 八丶持续集成 九丶获取cookie及请求处理 七丶接口自动化框架设计到开发 14.数据依赖问题从设计思路开始 说明:博主之前的测试的...
  • 自动化测试框架开发5步法

    千次阅读 2009-12-17 17:27:00
    2009年8月的《Automated Software Testing Magazine》杂志中有一篇文章名为《5 Steps To Framework Development》,介绍了常规的框架开发步骤: 1、Perform Initial Framework Design 初始框架设计 2、Create the...
  • Api自动化框架分享

    千次阅读 2016-01-03 22:05:18
    今天给大家分享的是最近开发的一个API自动化框架: 可能确实有点Low,分享出来一方面是希望能给到大家帮助,二来希望得到一些意见来做一些完善~! 框架定位1,数据驱动 2,简单流程覆盖,快速迭代 3,组合Case不...
  • 自动化测试框架概念

    千次阅读 2016-07-20 16:32:25
    1、自动化测试框架的产生 2、自动化测试框架常见的4种模式: 3、自动化测试的作用 4、自动化测试框架的设计核心思想
  • 主要讨论测试自动化。说是专家讨论会,我参加实在是惭愧,我对测试的理解实在是太浅薄了。只是因为在博客上发表了一些谬论才收到邀请。想着可以帮助公司去接受一些新的思想,而自己也可以结识一些朋友,便去了。对于...
  • APP 自动化框架设计思路分享

    千次阅读 2016-01-03 21:43:19
    APP自动化框架设计分享欢迎一起交流,一起进步 可以关注我的微信公众号:扫描二维码关注哦:点我关注框架需要解决的问题:• 渠道包->多渠道包核心用例自动化• 多设备覆盖安装,安装卸载更新等测试• 多设备核心...
  • Python自动化测试框架

    千次阅读 2019-10-27 14:02:15
    Python自动化测试框架 1 什么自动化测试框架 什么是工具——工具(Tools)类似框架,只是工具屏蔽了底层的代码,提供单独的操作界面,以此供用户操作,比如:Selenium IDE、Postman、QTP、Jmeter、LoadRunner等自动...
  • python自动化框架(一)

    千次阅读 2021-01-18 14:59:19
    最近刚入职一家新公司,要求我们部门搭建一套自动化框架,看过很多网上的开源框架,包括Robot Framework、Pytest、UnitTest等,发现这些框架都不适合我司的业务要求,经过深思熟虑,我决定独自设计一套属于我们公司...
  • 自动化框架到底是什么?

    千次阅读 2020-09-30 21:16:46
    微型,小型和中型企业(SME)使其测试过程自动化变得越来越必要,但是最关键的方面是选择正确的测试自动化框架。网上各式各样的介绍是否让你眼花缭乱?接下来,让我从实践出发,介绍一下什么是自动化测试框架。 ...
  • 分层自动化:["分层设计思想","分层设计实战","分层case编写","流程调节"] Unittest与PO结合:["Unittest的使用","assert断言","HTMLTestRunner","如何批量管理case","失败截图处理","项目结合"] 数据驱动的使用:...
  • 自动化测试】自动化测试框架与工具

    千次阅读 多人点赞 2021-05-06 21:01:01
    文章目录1)什么是自动化测试框架?...1.4 判断是否需要自动化测试适用项目:2)自动化测试框架的类型2.1 自动化测试框架的分类2.1.1 基于模块的测试框架:2.1.2 库架构测试框架2.1.3 数据驱动测试框架2.1.4 关键字驱动
  • JSON Wire协议是一个抽象规范,定义了用户在自动化脚本里操作该如何映射到Selenium或HTTP请求和响应中,通过JSON Wire协议可以在HTTP服务器之间传输信息,Language Bindings和WebDriver就是通过HTTP协议传输JSON数据...
  • 自动化测试框架思路

    千次阅读 2016-08-14 14:24:20
    第一次写类似的文章,语言组织能力不够,就从最简单的开始一路白话文把自己搭框架时的想法一路写下来,可能比较啰嗦,...所以自动化测试框架的定义为:由一个或多个自动化测试基础模块、自动化测试管理模块、自动化测试
  • Java接口自动化测试框架设计-1-开发环境的搭建

    万次阅读 多人点赞 2018-05-21 23:58:05
    废话不多说,刚开始,尽量详细,通过一步一步,手把手教会你搭建接口自动化测试的基础项目环境。 一.前提条件 我这里来一个约定,有以下前提条件。1.本机环境安装了maven并配置环境变量2.本机环境安装了Eclipse...
  • Appium移动自动化框架初探

    万次阅读 2016-06-22 11:17:56
    本文简要介绍了appnium自动化框架的架构、加载流程、支持语言、相关配置,以及元素定位工具等。 官方网站: http://appium.io Appnium是由SauceLabs支持,一家业界领先的测试产品公司, 有成熟的企业...
  • 5种比较常见的自动化测试框架

    千次阅读 2020-10-26 14:34:43
    自动化测试框架
  • 基于java接口自动化框架

    千次阅读 2018-03-07 16:53:11
    框架简介 APIAutoTest是处理API接口的轻量级自动化测试框架,Java语言实现,基于TestNG测试框架,支持持续集成,自动构建与测试。框架介绍数据驱动设计,使用TestNG中的@DataProvider读取Excel中存储的自动化测试...
  • 1,web自动化说起来很多人都会直接想到UI自动化这个设计,很少有人直接第一个概念是接口自动化,接口测试的开发更加快捷方便,维护工作比起UI自动化更加容易,例如:有一个项目,做完第一周期以后,后面需求变动,...
  • STAF(SoftwareTesting Automation Framework)是一个由IBM开发的开源、跨平台、支持多语言且基于可重用的组件来构建的自动化测试框架,而这一系列的组件都是一些可以处理调用、资源管理、监视等一些列的服务组成...
  • 基于python的自动化测试框架搭建

    万次阅读 多人点赞 2017-12-04 17:35:10
    自己动手搭自动化测试框架
  • 测试自动化并不像某些人想象的那样容易或经济。仅应在必要和适用的情况下使用。而且,最重要的是,它不能替代手动测试。相反,它补充了手动测试。自动化框架和正确的测试自动化工具对于增强测试自动化...
  • 1、为什么开发这个框架呢? ...自动化测试接触过java+selenium; 在网上或者测试行业现阶段分析了一下,python这门语言学习起来简单一些但是功能强大,最后2016年下半年决定在空余时间学习研究pytho
  • 此篇文章是,如何从零搭建一个属于自己项目的自动化测试框架(第一篇到第十一篇)文章的总录。 思考:我们需要一个什么样的框架 既然要搭一个框架,我们首先得弄明白我们需要一个什么样的框架,这个框架要支持...
  • RF这个自动化框架原理

    千次阅读 2018-07-31 16:47:43
    RF这个自动化框架在很早的时候我就有看,不过当时被网络上坑爹的教程欺骗了,所有的网络教程写的都是用它的ride来编写脚本,局限性太大,当时也很懒,并没有去看官方文档,所以一直感觉这框架很无聊,不如直接去敲...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 544,100
精华内容 217,640
关键字:

自动化框架开发