一、前言

大概在 4 个月前我发布过那篇Web 应用并发自动化测试,其实在 web 之前我还做过移动端的并发自动化尝试,但遇到太多坑了,可能是之前对技术或工具的不熟悉,所以当时也没做出来,后面经过这段时间的努力,终于让我做出个小框架来同时支持在 windows 上 android 的并发自动化测试,还有在 Mac 上的 iOS 和 Android 的并发自动化测试,好吧,说正题,附上框架图


二、准备

为了能够正常运行这个小框架,还是得有一些东西支持的,列举一下大概用到的工具:
1、appium
2、robotframework
3、XQuartz

其中我要说 XQuartz 是什么,其实就是 xterm,一个在 mac 上运行的终端命令行工具,为什么用到它,因为它支持传入参数后再启动进程,这好比 windows 上的 start 命令,但由于 mac 系统本身就是不像 windows 那样有 start 命令的,所以就用到它了,比如下面这句

xterm -e /bin/bash -c 'sh run_appium_ios.sh‘

这样,xterm 就能在一个新窗口中开启一个新进程来运行 appium,那启动多个 appium 的话也就不会冲突了,下面会有具体演示

先简单说说原理和一些相关知识

首先是启动 appium,由于多台真机设备的测试,当然是要用到多个 appium,其实对于多设备用 appium 做并发自动化测试,为了解决冲突,无非是解决两个问题

a、设备 udid 向 appium 发送以识别是哪台设备要做自动化测试
b、appium 启动所占用的端口

其实 a 的话有尝试过做指定设备的自动化测试就知道,b 的话无非是 appium 用到的服务端口(默认 4723),对应还有 android 端的 bootstrap 的端口以及 iOS 端口的 webdriveragent 的转发端口,关于端口这边问题,其实 appium 1.6.5 之后都是没问题的,大家看看下面的命令

android(run_appium_ad.sh):

appium -p $1 -bp $2 -a $3

iOS(run_appium_ios.sh):

appium -p $1 --webdriveragent-port $2  -a $3

其实这两句命令就是我用来启动 appium 的 sh 脚本,往里面传参数就好,其中那个--webdriveragent-port 就是 webdriveragent 的端口转发的指定端口,比如在 iOS 端上的 webdriveragent 启动服务后默认是手机 ip:8100,那你本地就可以通过一个如 8101 的端口去映射手机的 8100 端,这样就能做到访问手机上的 webdriveragent,大家都知道 webdriveragent 在 iOS 端自动化测试中的作用吧,社区里面也有很多介绍,这里就不列举了,也顺带提一下一般 appium 是用自己目录下面的 webdirveragent 来 build 的,所以在此之前需要去里面添加证书和重命名包名,不然 build 不成功就不可行了,以及个人开发者 id 最多是 3 个设备同时 build,多了就要用企业开发者账号和证书了,这点有玩过的同学应该知道,这里就不具体说 webdriveragent 相关的操作,社区有很多帖子,多点用一下社区的搜索功能吧

三、演示及分析过程

不多说,先放图,首先是自动启动对应 android 的 appium 服务

这就是第一句命令

python run_server.py -o android

那就是说说 run_ server.py 是干嘛的

how to use it
  -h   help
  -o   the device os.likes ios,android
  -n   the num of starting appium server

它就是用来根据输入的参数来启动 appium 服务的,那参数哪里来的,大家可以去下载代码边看我说边分析,-o 是指测试设备的系统,首先在 run_server.py 中有一句

run_server.py:
divlist=get_info.get_devices(plat)

根据输入的参数,里面会去指定命令来获取当前连接到电脑的对应操作系统的设备数,来启动指定的 appium-server 的数量,比如 iOS 端的

python run_server.py -o ios -n 3


就是假设当前连接到电脑的 iOS 端手机只有两台,但我还是想启动 3 个 appium 服务来应对我可能接入第三台设备,那我就多启动一个吧,当前里面也会有逻辑判断,那回上面那句命令的作用,看看源码

get_info.py:
def get_devices(auto=None):
    ADB=adb_helper.AdbHelper()
    devices=[]
    if auto in ["iOS","ios"]:
        output=os.popen("idevice_id -l").readlines()
        for idevice in output:
            idev=idevice.split('\n')[0]
            devices.append(idev)
        return devices

    output=ADB.getConnectDevices()
    #print output
    for line in output:
        if line['state'] in ["device","device\r"]:
            dev=line['uuid']
            devices.append(dev)
    return devices

这个方法就是用来根据输入的参数获取当前连接到电脑的设备数(其实就是设备列表的长度)和设备的 udid,android 的用到是之前社区一位朋友提供的 adb_helper,其实用到就是 adb devices 命令,里面做了一些过滤,更好地获取当前设备数,iOS 则用到 idevice_id -l 获取,这样的话前文提到的 udid 也已经到手了,udid 解决了,接下来就是端口问题,其实更好办

get_info.py:
aport=4723
bport=5723
wport=8101
iport=14723
def start_server():


        if plat in ["iOS","ios","Android","android"]:
            if osplat in ["Mac"]:
                if plat in ["iOS","ios"]:
                    mange_port.kill_port(wport)
                    mange_port.kill_port(iport)
                    run_app="xterm -e /bin/bash -c 'sh run_appium_ios.sh {0} {1} {2} ' &".format(iport,wport,ip)
                    iport=iport+1
                    wport=wport+1              
                else:
                    mange_port.kill_port(aport)
                    mange_port.kill_port(bport)
                    run_app="xterm -e /bin/bash -c 'sh run_appium_ad.sh {0} {1} {2}' &".format(aport,bport,ip)
                    aport=aport+1
                    bport=bport+1
            else:
                mange_port.kill_port(aport)
                mange_port.kill_port(bport)
                run_app="start run_appium.bat {0} {1} {2}".format(aport,bport,ip)
                aport=aport+1
                bport=bport+1
            os.system(run_app)


        elif plat in ["grid","Grid"]:
            mange_port.kill_port(bport)
            if osplat in ["Mac"]:
                run_app="xterm -e /bin/bash -c 'sh run_appium_grid.sh {0} {1} {2} {3} {4}' &".format(ip,aport,bport,div,conf_mac)
            else:
                run_app='start run_appium_grid.bat {0} {1} {2} {3} {4}'.format(ip,aport,bport,div,conf)
            os.system(run_app)     
            aport=aport+1
            bport=bport+1


        else:
            print "Not support this os device!"

(上面有省略部分代码),其实 android 的端口是用 4723 开始和 5723 开始,iOS 是用 14723 开始和 8101 开始,每启动一个 appium 服务就对应加 1,对于端口的使用,假设我现在要用 4723 端口,但之前有程序在占用怎么办,我现在用来一种粗暴的方法,就是把占用端口的那个进程 kill 掉,然后用来启动 appium,其实就是 mange_port.kill_port 方法,那就畅通了,所以一般不会用系统默认的端口范围(1-1024),当然,这个是可以改的,后文会提到,这里面还有个 ip 就是指当前机器的 ip,一般不是 127 那个,是真实 ip 那个,用来干嘛,其实框架是支持 android 用 selenium-gird 来做并发自动化测试的,但我不建议用这种方法,但你想用也可以,appium 和 selenium-grid 怎么用百度大把,和我之前写的那篇 web 并发测试是一样的原理,但是如果 grid 是远程的话,就要用到宿主机的真实 ip 了,所以这里启动 appium 我就不用 127 那个了,理论上也是可以的,但有长远考虑

(2017.07.)

好吧,接下来就演示一下怎么执行并发自动化测试的,我演示的时候用的是两台 iOS 设备和两台 Android 设备,也看过 web 并发那篇也知道,其实在用例上面也要做一些手脚,如下图
在 robot 上用 appium,第一个关键字无疑是 open application,那需要传入的参数主要有两个,一个是 appium 服务的地址,一个就是 udid,这样,设备和 appium 就能对应起来,默认是设备列表第一台设备指向第一个 appium(4723)服务,后面就加 1 继续指向直到所有设备都有指定的 appium 为止,这一块其实是用例执行的时候设置的,对于设备的顺序,也说一下,一般是最后连接到电脑的那台设备就是设备列表的第一台设备,以此类推吧,好用例设置的图

android:

iOS:

当然之前做并发自动化就是用到 robot 的用例标签功能来做分发了,所以也标记上

搞定了之后,就是执行自动化测试了,那第二句命令来了
android:

执行情况:

iOS:

执行情况:

第二句命令:
android:

python robot_mutil_dev.py -s /Volumes/sd_card/lunkr_test_git/AutoTest_Mutil  -t test2,test1 -o android

iOS:

python robot_mutil_dev.py -s /Volumes/sd_card/lunkr_test_git/AutoTest_Mutil  -t tag1,tag2 -o iOS

原理和之前 web 那篇是一样的,就是把参数补全

how to use it
-h   help
-s   the testsuite or testcase path
-t   taglist,likes "tag1,tag2", split by ,
-o   the device os.likes ios,android
-r   the remoteurl,use for gird

当然,你想 android 和 iOS 一起玩也是可以的

那也说说 robot_mutil_dev.py 的代码,拿 iOS 的那部分来说吧:

elif testos in ["iOS","ios"]:
    i=0
    divlist=get_info.get_devices("iOS")
    for tag in taglist:
      wdport=wdhost+str(iport)+"/wd/hub"
      #print wdport
      booll=check_server.check(ipaddr,iport)
      if booll==0:
        print "the appium server by {0} is not start,please check it".format(wdport)
        sys.exit(0)
      cmd='pybot -i {0} -o ./resultDir_ios/output-{0}.xml -l ./resultDir_ios/log-{0}.html -r ./resultDir_ios/report-{0}.html --variable remote_url:{2} --variable udid:{3} {1}'.format(tag,testsuite,wdport,divlist[i])
      p=multiprocessing.Process(target=run,args=(cmd,))
      lprocess.append(p)
      iport=iport+1
      i=i+1

wdhost 就是 appium-server 的地址,iport 就是 14723 开始的那个,在执行用例之前,通过 check_server.check 方法来检查 appium 有没有启动,没启动就不跑退出,这里怎么判断,看看 check_server.check 的代码:

#coding=utf-8
import os 
import sys
import requests
import time 

def check(ip,port):
    flag=1
    ct=0
    while(flag and ct<10):
        try:
            r = requests.get(url='http://{0}:{1}/favicon.ico'.format(ip,port))
            if r.status_code==200:
                print "the appium respone code is {0}".format(r.status_code)
                flag=0
                return 1
            else:
                ct=ct+1
                print "the appium respone code is {0}".format(r.status_code)
                print "the code is not equels 200 ,it would something wrong,please check it,,time:{0} ".format(ct)            
                time.sleep(3)
        except:
            ct=ct+1
            print "appium server is not start by port:{0},try to check again now ,time:{1}".format(port,ct)
            time.sleep(3)

    if ct==10:
         return 0

其实就是请求一下 appium 存不存在,favicon.ico 是一张草莓🍓图片,一般 appium 启动起来之后访问它返回 200 就正常了,那就可以用来判断 appium 的启动情况了,一般会检查 10 次,每 3 秒一次,因为用 jenkins 来做构建的时候也要判断一下 run_server.py 启动的 appium 成不成功,启动不成功还用跑个鬼并发自动化,所以还是得检查的,然后就是用 --variable 来把 appium 的地址和 udid 传入到测试用例当中,那就能执行并发自动化测试,对于并发,这里现在是用多进程的方法,之前 web 那篇是用多线程的,为什么这里用多进程呢,我在 mac 上面跑并发的时候一开始是用多线程尝试的,但是多线程启动之后,第二个线程不知道为什么总是要等第一个线程执行完之后才会去执行,就 mac 上面会这样,真的,我后面换成用多进程就没事了,可能和系统的资源分配有关系吧,毕竟进程间的资源是独立的,所以后面考虑到兼容用,就直接用多进程了,然后下面就是执行的代码

for p in lprocess:
       p.daemon = True
       p.start()

   for p in lprocess:
       p.join()

   if testos=='None':
      pass
   elif osplat!="Windows":
       sleep(2)
       if testos in ["iOS","ios"]:
         os.system(u"rebot --output ./resultDir_ios/output.xml  -l ./resultDir_ios/log.html -r ./resultDir_ios/report.html --merge ./resultDir_ios/output-*.xml")
       elif testos in ["Android","android"]:
         os.system(u"rebot --output ./resultDir_ad/output.xml  -l ./resultDir_ad/log.html -r ./resultDir_ad/report.html --merge ./resultDir_ad/output-*.xml")
       else:
          os.system(u"rebot --output ./resultDir/output.xml  -l ./resultDir/log.html -r ./resultDir/report.html --merge ./resultDir/output-*.xml")
   else:
       sleep(2)
       if testos in ["Android","android"]:
         os.system(u"rebot --output .\\resultDir_ad\\output.xml  -l .\\resultDir_ad\\log.html -r .\\resultDir_ad\\report.html --merge .\\resultDir_ad\\output-*.xml")
       else:  
         os.system(u"rebot --output .\\resultDir\\output.xml  -l .\\resultDir\\log.html -r .\\resultDir\\report.html --merge .\\resultDir\\output-*.xml")
   sleep(2)
   print "Test Finish"

同样的,等待所有的进程都执行完成后,就会通过 robot 的合并测试报告的方法将多个进程执行的自动化测试报告合并起来,有一些注意事项像图片的可以看Web 应用并发自动化测试,这里就不在多说了,就这样,两句命令其实就可以搞定移动端的并发自动化测试了,小框架还会根据不同的操作系统适配不同的命令,目前测试是支持 mac 和 win7,win10 我还没试,理论上是 ok 的,win7 上可以执行 android 的并发自动化测试

四、亮点和坑

坑:
1、刚才提到的端口问题,是通过 kill 掉占用的进程来释放端口,其实我一开始是想通过跳过端口的方法的,但是跳过的之后,就得找一个地方存着对应设备和对应的 appium 服务端口,而两个脚本之间是没什么关联的,我本来也可以写个配置文件来将它们关联起来,配置文件就可以对应 udid 和 appium,但这样显然有点麻烦,我还想过拔出设备的时候要不要自动 kill 掉对应的 appium 服务,那又回到上面的问题了,其实就是缺个管理平台
2、在执行并发自动化的时候,android 这边有低概率会出现安装程序包失败,其实就是刚好包被占用的问题,这边的话注意调度就用,或者把程序把分开不同地方放就好了
亮点:
1、这个是我在调试中无意发现的,就是我现在已经启动来 appium 服务,我现在只用两个 iphone 测试,其中一台我不想用了,换成 ipad,再执行自动化测试的时候无需再做任何操作,只要保证 appium 服务正常,设备连接正常,就是可以直接运行并发自动化测试了,这个亮点简单的说就是可以随意的更换测试设备而且无需做大量操作,当然 appium 服务不够怎么办,刚才提到我是会 kill 掉进程的,在运行一次第一句命令就好了

我还打算接入 macaca 的,因为之前就是写了 macaca 的 rf 库,原理是一样的,就是启动的命令差异,或者还有一些坑,后面慢慢在看吧

五、最后说说

上面的坑那里提到,我就是缺个管理平台,其实现在在我脑海里是有这个平台的 demo 的,就是一个支持 ios 和 android 执行并发自动化和专项测试等测试管理的 stf 管理平台,说到这里,对于测试方案或测试技术的设计,我提一句 “用产品的思维去做测试”,你要做一个测试方案或一个测试工具,首先你要知道这个工具的目标用户是谁,要解决用户什么问题,最后能带来什么价值,测试人员就是测试工具的用户,测试工具或方案就是用来解决测试人员在测试工作效率或者流程管理上的问题,能够带来降低测试成本和提升产品质量的价值,贴近业务,把握痛点,按照这种思路来开发测试工具、框架或方案,一般都会比较高可用。接下来的时间将花在这个管理平台上面了,我还用 Axure RP 画了个小 demo,但是要做这个平台不容易啊,我还得慢慢补充自己的技术知识和业务知识了,在 IT 这个行业混,总得弄一个能为自己代言的作品吧,嗯,这就是我接下来要做的,之前的一些技术方案,都是几天就做出来的,所以其实压根没有解决过什么根本问题,比如 mock,我见过真正的 mock 平台之后,我都不敢说我会写 mock 服务器,还是静态代码扫描,diffy 校验,docker 微服务架构自动编排,还有后面的移动端无线技术,iOS11 和 xocde9 都已经支持无线调试了,客户端无线自动化测试是必然的,还有团队在倡导的测试工程化,现在的这个,估计花 1 年甚至几年都不知道搞不搞得定,好好加油吧,说了那么多次,现在缺的就是深度,这是要一点一滴地积累吧,好吧,最后,大家如果拿去用的话,用的过程中有问题,或者是有 bug 可以在这里反馈,也可以直接到 github 上面提,谢谢大家来,欢迎大神指导,欢迎大家提建议

附录:

github 地址:
并发框架
MacacaLibrary

设计思路可以看:
浅谈测试工程化 - 以并发自动化框架为例


↙↙↙阅读原文可查看相关链接,并与作者交流