美文网首页Appium玩转手机之python和AI
Android Appium+python自动化框架

Android Appium+python自动化框架

作者: h080294 | 来源:发表于2017-09-13 00:02 被阅读2222次

    一直假装没有时间整理自动化的东西,想来这笔债不能总是拖。大概年前的时候组里说要尝试着进行自动化方面的工作,就做了相关方面知识的学习。当然对于一个普通的黑盒测试人员来说,我们选择了从UI自动化入手。

    一、需求——确定框架

    开始做Android自动化只为了解决“多台设备同时自动执行一套测试代码,并输出相关日志或者图片的log”。因为大家的代码能力都不高,关于使用哪种工具,并没有经过太多的探讨,选择了较容易入手的Appium + python unittest框架。

    以此框架就确定了下来:以python unittest为基础,并对Appium进行封装。
    

    后来加了临时需求,说既然要并发跑appium,那么也能用来并发运行monkey test。

    以此对应的需求:UI test + monkey test
    

    尝试了一些开源的框架,但不太适合我们产品和需求,因此只好自己写一个简单的框架出来。

    二、框架——搭建

    前面两篇文章已介绍了搭建过程以及环境问题,这里就不再冗余,具体请见:Android自动化 -- Appium环境搭建Mac OSX上的python环境

    官网上比较仔细地介绍了Android并发测试,详见Android并发测试

    三、模块介绍——所解决的问题

    由于我们的产品在使用前必须登陆,需要保证每个设备登陆不同的账号。细细想来,我们需要解决的问题大概有如下几点:

    1.即可运行appium又可运行monkey,但又不能同时运行这两个任务 --> 任务划分,区分monkey和appium服务
    2.根据多设备启动多个appium --> Appium Server模块
      |-- 负责处理Appium Server启动,停止,监听等 --> server的模块  
      |-- 负责处理多设备信息的模块 --> Device Object
      |-- 负责处理多个登陆用户信息的模块 --> User Object
    3.封装常用方法,统一放在一个地方进行 --> Tester Object模块
    4.基于unittest管理testcase --> TestCaseManager模块
      |-- Testcase --> 可测试的用例
      |-- TestLoader --> 创建test suit
      |-- TextTestRunner --> RunTestManager模块
    5.处理异常 --> 沿用unittest框架的处理方式
    6.测试结果报告html形式输出 --> TestResult模块
    7.优化初始化如安装、卸载、登陆、处理权限、拷贝测试图片等等准备工作 --> BaseDevicePreProcess模块
    
    1. 任务划分,区分monkey和appium服务

    这里是这么构思的,构建一个SimpleHTTPServer,每次执行任务前先请求Server,Server端判断当前是否有正在执行的任务,如果有正在执行的,就返回个错误信息;如果没有,就开始执行任务。

    class HttpServerHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
        //省略好多内容
        def run(self, params):
            if share.get_if_run() == True:
                result_dict = {'code':1002,"data":{"message":"已经有一个任务在执行","taskid":"%s" % share.get_taskid()}}
                self.set_response(result_dict)
                return
            if params.has_key('mode') == False:
                result_dict = {'code':1003,"data":{"message":"缺少mode参数"}}
                self.set_response(result_dict)
                return
            elif params['mode'][0] != "monkey" and params['mode'][0] != 'autotest':
                self.set_response({'code':1004, "data":{"message":"mode参数错误"}})
                return
    
            try:
                set_run_manager(RunTestManager(params['mode'][0]))
                self.taskid = get_run_manager().task_id
                share.set_taskid(get_run_manager().task_id) #设置全局共享taskid
                share.set_if_run(True)
                thread = threading.Thread(target=get_run_manager().start_run)
                thread.start()
                result_dict = {'code':0,"data":{"taskid fuck":"%s" % self.taskid,"message":"开始执行%s任务" % params['mode']}}
                self.set_response(result_dict)
            except Exception, e:
                traceback.print_exc()
                get_run_manager().stop_run()
    
    2. 根据多设备启动多个appium
    // 一个例子
    $ appium -p 4736 -bp 4836 -U b33aa57c --session-override
    // -p Appium的主要端口
    // -bp Appium bootstrap端口
    // -U 设备id
    

    启动多个设备需要运行时根据不同的端口进行appium配置。所以得先有个处理devices的模块。按照惯例,把安装包和设备的详细信息和用户登陆账号等以list方式写进config文件里,后面再读出来。

    // congif.yaml 文件
    NiceAPK: /Users/xxxxx/com.nice.main.apk    # 测试包的路径
    Devices:
     - deviceid: 5HUC9S6599999999    # 设备识别adb devices的值
       devicename: OPPO_R9M    # 设备的名称,用于区分
       serverport: 4723    # -p Appium的主要端口,设备之间不能重复
       bootstrapport: 4823    # -bp Appium bootstrap端口,设备之间不能重复
       platformname: Android    # desired_caps
       platformversion: 5.1    # desired_caps
       server: 127.0.0.1     # 地址
    
     - deviceid: 7c404969
       devicename: OPPO_A33
       serverport: 4724
       bootstrapport: 4824
       platformname: Android
       platformversion: 5.1.1
       server: 127.0.0.1
    
    Users:
     - uid: 33333333333
       username: test01
       mobile: 33333333333
       password: 333333
    
     - uid: 44444444444
       username: test02
       mobile: 44444444444
       password: 444444
    

    然后分别创建Device object和User object(和Device object一致)

    class Device(object):
        def __init__(self, deviceid):
            self._deviceid = deviceid
            self._devicename = ""
            self._platformversion = ""
            self._platformname = ""
            self._bootstrapport = ""
            self._serverport = ""
            self._server = ""
    

    然后我们可以通过DataProvider来实例化设备和用户信息

    class DataProvider(object):
        @classmethod
        def load_devices(cls):
            cls.devicenamelist = []
            for device in cls.config['Devices']:
                deviceobject = Device(device['deviceid'])
                deviceobject.devicename = device['devicename']
                deviceobject.serverport = device['serverport']
                deviceobject.bootstrapport = device['bootstrapport']
                deviceobject.platformname = device['platformname']
                deviceobject.platformversion = device['platformversion']
                deviceobject.server = device['server']
                cls.devices[deviceobject.deviceid] = deviceobject
                cls.devicenamelist.append(device['devicename'])
            Log.logger.info(u"配置列表中一共有 %s 台设备" % len(cls.devices))
    
        @classmethod
        def load_users(cls):
            for user in cls.config['Users']:
                userobject = User(user['uid'])
                userobject.username = user['username']
                userobject.mobile = user['mobile']
                userobject.password = user['password']
                cls.users.append(userobject)
            Log.logger.info(u"配置列表中一共有 %s 个用户信息" % len(cls.users))
    

    有了devices和users,后面我们就可以创建个server类来处理appium server的启动、停止、监听设备等等功能。例如根据多设备来启动多个appium

    class Server:
        def __init__(self, deviceobject):
            self.logger = Log.logger
            self._deviceobject = deviceobject
            self._cmd = "appium -p %s -bp %s -U %s --session-override" % (
            self._deviceobject.serverport, self._deviceobject.bootstrapport, self._deviceobject.deviceid)
    
    3. 封装常用方法的Tester类

    这里的tester用于存放driver、共用的封装方法,如点击、滑动、截视频、图像对比等等方法

    class Tester(object):
        def __init__(self, driver):
            self._driver = driver
            self._user = None
            self._device = None
            self._logger = None
            self.action = TouchAction(self._driver)
            self._screenshot_path = ""
            self.device_width = self._driver.get_window_size()['width']
            self.device_height = self._driver.get_window_size()['height']
    
    4. 管理TestCase的TestCaseManager类

    因为是基于python unittest,我们沿用unittest的方式,这里只添加一个参数化功能,用来方便我们指定所需要测试的集合。

    class BaseTestCase(unittest.TestCase):
        def __init__(self, methodName='runTest', tester=None):
            super(BaseTestCase, self).__init__(methodName)
            self.tester = tester
    
        @staticmethod
        def parametrize(testcase_klass, tester=None):
            testloader = unittest.TestLoader()
            testnames = testloader.getTestCaseNames(testcase_klass)
            suite = unittest.TestSuite()
            for name in testnames:
                suite.addTest(testcase_klass(name, tester=tester))
            return suite
    

    然后创建个TestCaseManager来处理不同种类的测试类型

    class TestCaseManager(object):
    
        def __init__(self, tester):
            self.compatibility_suite = unittest.TestSuite()
            self.testcase_class = []
            self.load_case()
            self.tester = tester
    
        def load_case(self):
            testcase_array = []
            testsuits = unittest.defaultTestLoader.discover('testcase/', pattern='test*.py')
            for testsuite in testsuits:
                for suite in testsuite._tests:
                    for test in suite:
                        testcase_array.append(test.__class__)
            self.testcase_class = sorted(set(testcase_array), key=testcase_array.index)
    
        # 兼容性测试用例
        def compatibility_testsuite(self):
            for testcase in self.testcase_class:
                self.compatibility_suite.addTest(BaseTestCase.parametrize(testcase, tester=self.tester))
            return self.compatibility_suite
    
        # monkey自动化
        def monkey_android(self):
            self.tester.run_monkey(200,1000)
    
        # 功能性测试用例
        def functional_testsuite(self):
            pass
    
        # 单独运行一条指定的用例
        def signal_case_suit(self, test_myclass):
            suite = unittest.TestSuite()
            suite.addTest(BaseTestCase.parametrize(test_myclass, tester=self.tester))
            return suite
    

    那么实际要运行的时候,我们在TextTestRunner传个参数来指定运行的suit就可以了

      suite = TestCaseManager(tester).compatibility_testsuite()    # 运行兼容集合
      // suite = TestCaseManager(tester).functional_testsuite()      # 运行功能测试集合
      // suite = TestCaseManager(tester).signal_case_suit(test_case_001)    # 运行单条测试用例
      unittest.TextTestRunner(verbosity=2, resultclass=TheTestResult).run(suite)
    

    TextTestRunner的执行部分写在了RunTestManager里

    class RunTestManager(object):
        def start_run(self):
          //判断执行的类型,并调用start_run_test方法
        def start_run_test(self):
          //初始化tester object,并调用run方法并传tester object参数
        def init_tester_data(self, device, which_user):
          //初始化tester object
        def run(self, tester):
          //预处理(登陆、权限等流程),并调用unittest.TextTestRunner开始执行
        def stop_run(self):
          //结束运行,置server flag为false,表示当前不在有任务运行
    
    5. 处理异常

    沿用unittest框架的处理方式,在TestResult中重写addError、addFailure、addSuccess、addSkip等等一系列方法来满足我们自己的需求。特别是对addFailure的处理,我们需要详尽的知道哪台设备的哪里出了错,并且能输出截图和log日志。

       def addFailure(self, test, err):
            info = '************      - %s -!(Fail)    ***************' % self.tester.device.devicename
            self.logger.warning(info)
            info = 'Fail device:%s Run TestCase %s, Fail info:%s' % (self.tester.device.devicename, test, err[1].message)
            self.logger.warning(info)
            info = '***********************************************'
            self.logger.warning(info)
    
            # 失败截图
            mytest = str(test)
            simplename = clean_brackets_from_str(mytest).replace(' ', '')
            myscr = "Failure_%s" % simplename
            self.tester.screenshot2(myscr)
    
            # 失败日志
            list = traceback.format_exception(err[0], err[1], err[2])
            list_fail = []  # 列表包含要输出的错误日志信息
            # list_fail[0]='error:'
            # list_fail[1]=list[2:3]
            # list_fail[2]=list[-1]
            list_fail.append(list[-1])
            list_fail.append(list[2])
    
            self.__class__.totalresults[self.deviceid]['failtestcase'] = self.__class__.totalresults[self.deviceid]['failtestcase'] + 1
    
            self.__class__.detailresults[self.deviceid][test]['result'] = 'Fail'
            self.__class__.detailresults[self.deviceid][test]['reason'] = list_fail
    
    6. 测试结果报告html形式输出

    同上面的异常处理,结果的输出也放在TestResult来执行。不知道当时怎么想的,输出处理这块用了pyh。所有的表格都是一点点画出来的,心很累,还抽空搞了下css和js,美化了一下样式。代码很长就不贴了,基本是一个div一个div写出来的。直接看源码就好了,这里不展开啦。



    样式上还有bug。。。。因为一些设备意外退出导致的,这个暂时won't fix。。。。

    7. 关于预处理部分,优化初始化过程

    这里有很多的工作,比如安装,处理每台设备登陆过程,处理登陆界面的权限问题,拷贝测试图片等等。毕竟只有登陆了才能进行测试!!!
    1)因为不想每次启动appium都要安装setting\unlock\ime等apk,所以修改了Appium源码,不让他自己安装。运行的时候,由我们自己的函数处理安装过程

    // 干掉自动安装
    文件: /usr/local/lib/node_modules/appium/node_modules/appium-android-driver/lib/driver.js,注释以下几句代码
    await this.adb.uninstallApk(this.opts.appPackage);
    await helpers.installApkRemotely(this.adb, this.opts);
    await helpers.resetApp(this.adb, this.opts.app, this.opts.appPackage, this.opts.fastReset);
    await this.checkPackagePresent();
    
    文件:/usr/local/lib/node_modules/appium/node_modules/appium-android-driver/build/lib/driver.js 注释以下几句代码
    return _regeneratorRuntime.awrap(_androidHelpers2['default'].resetApp(this.adb, this.opts.app, this.opts.appPackage, this.opts.fastReset));
    return _regeneratorRuntime.awrap(this.adb.uninstallApk(this.opts.appPackage));
    return _regeneratorRuntime.awrap(_androidHelpers2['default'].installApkRemotely(this.adb, this.opts));
    return _regeneratorRuntime.awrap(this.checkPackagePresent());
    
    文件:/usr/local/lib/node_modules/appium/node_modules/appium-android-driver/lib/android-helpers.js 注释以下几句代码
    await adb.install(unicodeIMEPath, false);
    await helpers.pushSettingsApp(adb);
    await helpers.pushUnlock(adb);
    
    文件 /usr/local/lib/node_modules/appium/node_modules/appium-android-driver/build/lib/android-helpers.js 替换以下几句代码
    return _regeneratorRuntime.awrap(helpers.initUnicodeKeyboard(adb)) 替换为return context$1$0.abrupt('return', defaultIME);
    return _regeneratorRuntime.awrap(helpers.pushSettingsApp(adb)); 替换为return context$1$0.abrupt('return', defaultIME);
    return _regeneratorRuntime.awrap(helpers.pushUnlock(adb)); 替换为return context$1$0.abrupt('return', defaultIME);
    

    2)由于不同设备的安装会有极大的不同,比如有的需要确认usb安装,有的设备会询问你是否安装;高API会弹授权提示,低API没有提示等等不协调的地方有很多。因此统一写个了PreProManager类来管理设备,目的是给每一台设备分配他自己的执行函数

    class PreProManager(object):
        def __init__(self, tester):
            self.tester = tester
            self.deviceid = self.tester.device.deviceid
    
        def device(self):
            if self.deviceid == "5HUC9S6599999999":
                return OPPOR9PreProcess(self.tester)
            elif self.deviceid =="7c404969":
                return OPPOA33PreProcess(self.tester)
    

    然后写个BaseDevicePreProcess基类描述预处理过程,上面的各个设备的执行函数直接继承这个基类,并复写里面的一些方法就行了

    class BaseDevicePreProcess(object):
        def __init__(self, tester):
            self.tester = tester
            self.driver = self.tester.driver
            self.action = TouchAction(self.driver)
            self.user = self.tester.user
    
        # 开始预处理流程
        def pre_process(self):
          // 卸载、安装等等
    
        # 安装流程
        def install_app(self):
            self.driver.install_app(DataProvider.niceapk)
    
        # 版本升级
        def upgrade_app(self):
          // ...
    
        # 该流程包括处理安装及启动过程中的各种弹窗,一直到可以点击login按钮
        def install_process(self):
            pass
          // 由子类复写 
    
        # 该流程包括点击login按钮到达登录页面,并登录
        def login_process(self):
          // 处理登陆流程 
    
        # 该流程包括登录成功后,对各种自动弹出对话框进行处理
        def login_success_process(self):
            pass
          // 由子类复写 
    
        # 对所有需要的权限进行处理,例如:相机、录音
        def get_permission_process(self):
            pass
          // 由子类复写      
    
        def data_prepare(self):
          // 写入测试data
    

    这里举例说明每个设备如何继承基类打造自己的专属处理流程

    from BaseDevicePreProcess import *
    class OPPOR9PreProcess(BaseDevicePreProcess):
        def __init__(self,tester):
            super(OPPOR9PreProcess, self).__init__(tester)  
    
        def install_process(self):
            // OPPOR9的专属登陆处理方法
            // 如果不需要复写,则直接用基类中的默认流程执行
    
        def login_success_process(self):
            // 处理登陆呦
            // 如果不需要复写,则直接用基类中的默认流程执行
    
        def get_permission_process(self):
            //  OPPOR9的专属处理授权问题方法呦
            // 如果不需要复写,则直接用基类中的默认流程执行
    

    总结:

    搭建框架的过程中,遇到了很多困难,不过很开心的是基本都解决了。现有的这些已经能在项目中run起来,但仍有诸多地方不够完善需要持续优化。
    慢慢加油吧
    代码已上传至github:
    https://github.com/h080294/appium_python_android.git

    关注获取更多

    相关文章

      网友评论

        本文标题:Android Appium+python自动化框架

        本文链接:https://www.haomeiwen.com/subject/twpcsxtx.html