两句命令搞定移动端 (iOS 和 Android) 真机并发自动化测试

一、前言

大概在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

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