优质广告供应商

广告是为了更好地支持作者创作

Android Appium+python自动化框架

一直假装没有时间整理自动化的东西,想来这笔债不能总是拖。大概年前的时候组里说要尝试着进行自动化方面的工作,就做了相关方面知识的学习。当然对于一个普通的黑盒测试人员来说,我们选择了从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 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 166,817评论 24 703
  • 前言 做Android端功能自动化已有2年多的时间了,使用过的功能自动化框架有Robotium、Uiautomat...
    海波笔记阅读 16,630评论 3 65
  • 优质广告供应商

    广告是为了更好地支持作者创作

  • 环境xode8.3 iOS10.3.2 Mac 10.12.11、安装homebrew2、安装:libimo...
    sunny_王阅读 5,391评论 0 4
  • 刚开始知道刘墉是室友说她喜欢刘墉的《萤窗小语》。 能记住刘墉的名字是多亏了《宰相刘罗锅》这部电视剧。 很敬佩这位华...
    朱黛阅读 426评论 0 0
  • 文/ 蓝山 在中国,恐怕没有什么节日比春节更重要的了。春节,旧一年的结束,新一年的开始,充满了希望和力量! 用老一...
    蓝山日记阅读 171评论 3 2
  • 梦想合伙人给予的启发畅想。 什么样的创意可以赚钱? 什么样的人可以为他人服务? 什么样的行动可以带来效应? 在微商...
    努力红阅读 220评论 0 0
  • 优质广告供应商

    广告是为了更好地支持作者创作

  • 生命之中总有些时候,会由于脑海中对过往的事情存在一定程度的记忆,最后在眼前的生活之中某些丝丝的相似牵动记忆,过去的...
    成长路阅读 56评论 0 1
  • 《那微笑》 ——瘦桶 那微笑 像洒落在野地的蒲公英 随风飘去 幸运的是我 曾陪它开放 它走了 像一只鸟...
    瘦桶阅读 313评论 0 60