作者:【友盟+】高级无线开发工程师 吴玉强、王飞
为了确保SDK线上运行的稳定性,我们需要在开发后进行SDK测试,而为了提高测试效率,而且在拓展新项目的同时能兼顾已有项目的稳定性,在有限的资源内解放测试人员到更紧急的项目中来,就需要一个自动化工具来完成工作,【友盟+】首创自动化工具,能够自动传不同参数、抓取输出数据,并自动验证数据准确性,输出结果,保障项目顺利稳定发布。
相对App的测试方案,市面上已经有非常多且成熟的UI级别的自动化测试框架,却鲜有针对SDK提供的自动化测试方案,原因是SDK属于为App提供服务的“插件”。一个App可接入一到多个SDK在内,而在项目中模块化是非常普遍的架构,所以SDK是针对细分功能提供服务的组件,有的提供数据服务、地图服务或节省开发成本的组件等等,这只能SDK开发者根据功能自行完成测试。
本篇说明的SDK测试方案是针对数据服务的SDK功能覆盖,皆包含SDK的API、网络数据及缓存相关的逻辑测试,即非UI的纯数据逻辑的覆盖。
本篇是自动化测试基础上的延伸,相对安卓系统可以便利的通过adb指令控制如App安装、卸载、退出应用等“系统”级操作,iOS在控制App层面上只能通过一些间接的手段完成上面几点需求,为了易于维护,在控制器中以有限状态机模式进行了构造,以便于后续增加更多的操作状态和测试用例。
一、测试框架概览
1、测试框架
整个测试流程就如下面描述的有向图,以Pytest驱动客户端执行任务,然后将客户端输出的请求数据进行截取处理,而后验证是否通过测试用例。
2、Android端测试框架
Android可以使用adb命令与app进行数据上的通信,如发送广播,启动Activity等。同时也可以使用shell命令对配置文件进行修改,再进行gradle编译,实现对app级别参数的修改,从而完成不同参数对app程序影响的验证。
3、iOS端测试框架
iOS由于系统特性,无法如安卓系统灵活运用系统命令来操作App或SDK,所以以一个Socket连接Server端进行通信。
另外在iOS系统上又可利用Runtime的特性,将传输的字符串转化为API调用,这样做的好处是将Socket模块和Runtime解析模块编入应用中就无需再次打包,只需Python端编好代码和测试case,所有的功能调用都由两端约定的协议解析执行即可。
二、Android
1、SDK接口的验证
对于集成SDK的app,如果需要在App运行时,触发一个行为,可以通过广播来实现。可以根据action name完成对行为类型的分类,根据caseid完成对行为的区分。如下图所示:
根据上图示例如下:
os.system("adb shell am
broadcast -a com.umeng.auto.track --es param \""+ str(es) +"\"--ei caseId "+bytes(ei))
其中com.umeng.auto.track为广播的action name用以区分类别
ei为一个int数,相当于图中的caseid
es为参数内容,参数的协议可以自由定义,建议使用json类型,方便对不同类型的数据进行处理
这样,我们只需要在广播中以ei写一个switch语句,执行不同的行为,如果测试不同参数的效果,可以使用es传递内容。
2、Activity级别初始化的验证
如果使用广播,是没办法绑定生命周期,即如果SDK需要在Activity的onCreate()中进行一些类初始化操作,是没法进行控制的。所以对于这种情况就需要使用adb命令中的启动Activity命令,基本流程与广播类似,但是caseid的处理在onCreate()中:
根据上图示例如下:
os.system("adb shell am start -n " +self.pkgname + "/." + activity + " --es param \"" + str(es)+ "\"--ei caseId " +bytes(ei))
其中pkgname为包名,activity为activity的名字es为需要传入的内容,ei为一个int数,即caseId。
与广播方式类似,只是将switch放到了onCreate中,根据ei和es进行相应的操作。
3、Application级别的验证
以上说的两种方式几乎可以涵盖SDK测试的部分case,但是对于部分SDK,初始化需要在程序一启动的Application中执行,这时上面的两种方式显然满足不了需求。
这时有两套方案可以应对。如下图所示:
二次编译
如上图所示,左边的部分,我们可以通过修改Java文件完成对Appliction中内容的修改,如在Application中会有一些静态常量,使用python修改java文件中的常量,并重新运行:
defchangeConstant(self, source,des):
path =os.path.join(os.path.dirname(sys.path[0]),'autotestAndroid')
gradle_path =os.path.join(path,'app','src','main','java','deep','autotest','utils','Constant.java')
print'-----gradle_path----',gradle_path
ifos.path.exists(gradle_path):
build_file =open(gradle_path,'r+')
lines = build_file.readlines()
foriinrange(len(lines)):
line = lines[i]
if' '+sourceinline:
arr =line.split('=')
line = arr[0]+'='+des+";\n"
lines[i] = line
build_file =open(gradle_path,'w+')
build_file.writelines(lines)
p =buildprocess.CompileProcess(path)
p.start()
else:
print'nonono='+ gradle_path
使用这种方式的好处是:
[if !supportLists]•[endif]可以直接修改Application中的常量,如AppKey等,不用管是否执行了Application的onCreate()
[if !supportLists]•[endif]不用考虑外设情况
[if !supportLists]•[endif]同样适配对AndroidManifest.xml的测试
缺点是:
[if !supportLists]•[endif]需要绑定工程路径
[if !supportLists]•[endif]文件内容类型较多,容易出错,代码不具备通用性,有一定的二次开发难度
[if !supportLists]•[endif]需使用gradle重新编译,如工程较大,耗时较长
配置文件
除了上述方法,也可以在Application中读取一个SD卡配置文件,根据配置文件的协议进行对应的操作。每次只需更改配置文件的内容,并通过adb push放入SD卡指定路径中,然后重启App即可。
这样做的好处是:
[if !supportLists]•[endif]配置文件的协议可以随意定义,更灵活
[if !supportLists]•[endif]配置文件可以使用json格式,修改更简单
[if !supportLists]•[endif]只需推到SD卡,耗时更少
[if !supportLists]•[endif]不需要绑定工程路径
缺点是:
[if !supportLists]•[endif]只能在Application的onCreate之后进行,局限性较大。
[if !supportLists]•[endif]依赖外设SD卡
[if !supportLists]•[endif]AndroidManifest的测试无法使用。
三、iOS端SDK自动化测试流程
1、引入“守护”App
如「iOS端测试框架」所见,此时进行通信只有一个应用,这个应用就是我们用来测试SDK的Demo,通过这个宿主我们可以触发SDK提供的任何API,通过iOS runtime我们可以触发SDK的类方法、实例方法甚至是私有API,但这写都只局限于一个应用“沙盒”内,如上面说到的安装、卸载及App退出和切到后台就无能为力了,所以我们引入了另一个Demo(Watch Demo),通过两个Demo的协同操作满足“沙盒”之外的需求。
两个App互相唤醒和通信
如上面提到的,所有功能调用都基于约定的协议来执行的,协议的设计也是不断新增的测试需求改造的。
2、业务协议
最初Server端与客户端以测试用例的case id来区分需要触发的事件,后来case id所代表的含义太多,而且客户端也是以运行时不断调用Server端发送指令的形式表现执行的具体功能,所以转为一条执行序列更加灵活及方便扩展。
一个测试用例可分为多条执行序列,执行序列内的协议包含了需要进行的方法调用或事件的处理。
以Dplus为例,如下数据包含了部分操作的执行序列:
"operations":{
"$umeng_cloudayc_op9": {
"arguments": {
"param": [
"$umeng_cloudayc_op*"
]
},
"type": "class",
"class":"DplusMobClick",
"method": "track:"
},
"$umeng_cloudayc_op5": {
"arguments": {
"param": []
},
"next":"$umeng_cloudayc_op9",
"type": "class",
"class":"DplusMobClick",
"method":"clearSuperProperties"
}
},
"type":"invoke",
"description":"401",
"first": "$umeng_cloudayc_op5"
由于是针对SDK API测试的协议,所以协议内的格式以调用的类名、方法名及参数为主,再加上部分细节参数加以说明,如type是class则调用类方法,是instance是示例方法。
需要注意的是,这个队列的结构是个字典,以标识前缀$umeng_cloudayc_op作为一个子事件的key,value则是其执行参数。而且可以看到在参数param的value里也有和子事件的key类似的值,这里的设计也是为了满足部分嵌套调用的需求。举例来说,如此时需要通过一个接口验证之前缓存的数据是否发送正常,就要分三步,第一存储数据,第二将数据读出,第三将第二步的结果作为参数传入最后调用的接口即可,这样既能满足各种嵌套逻辑,又能实现远程构造客户端系统的实体对象作为参数进行接口调用。
回到上面的字典的结构,实际上在之前的协议格式使用的是数组作为执行序列的封装格式,不过在实际应用中无法满足灵活的要求,就如上面所说的组合的调用逻辑,有部分子事件是被动调用的,通过在其他事件内的参数检测来触发调用,如果是数组则无法控制这个执行序列的依赖关系。采用字典后,增加启动字段,在后续关联的子事件内,都会说明下一个执行的子事件,如果某个子事件是作为另外子事件的参数,则不会有next字段,因为它是被动触发的,不在执行队列之内。
在这个业务协议开发过程中,不断的根据测试需求进行改造、添加,从一开始的单一应用调用接口,到后面的多应用切换、前后台切换以及应用断开和重连,需要多套控制流程,在具体实现时,分散到了各个业务逻辑中,每增加一个控制都要兼容考虑是否会影响到其他模块,而且作为一个自动化测试“框架”,提前梳理好核心部分的流程会让之后更易于开发和维护,所以就引入了有限状态机的概念进行构造。
3、有限状态机
有限状态机(Finite-state machine)可用于模拟很多事物逻辑,顾名思义,它是一个有限的状态的处理逻辑,有下面几个特征:
状态数是有限的
在当前时刻只有一种状态存在
一个状态在满足某个条件后会切换到另一状态
而有限状态机整体可以归纳为四个要素:现态、条件、动作以及次态。
现态指当前时刻所表现的状态
条件又称为事件,即当前状态在满足这个条件后会触发一个动作,从而进行状态装换
动作即在现态满足条件后需触发的一系列操作,动作完成后即状态进行迁移。动作也可以忽略,在某些情况下,现态满足条件后,也无需执行任何动作就切换到新的状态。
次态是相对现态而言,表示了条件满足后迁移的状态,次态也可以与现态相同。
根据业务逻辑的特性及复杂程度,合适的使用有限状态机,可以使得逻辑表达清晰、封装及维护都很直观和方便。当一个业务包含的状态越多,就越适合使用优先状态机进行封装处理。
有限状态机应用非常广泛,如电子电路、编译器及网络协议TCP协议状态机等
需要注意的是要区分“动作”和“状态”,如果将“动作”也视为“状态”会导致编写状态机时产生问题。
4、有限状态机应用自动化测试
将业务逻辑应用到有限状态机,前提是需要熟悉对应的业务,并将其中的状态、动作和条件等抽离出来,然后再做进一步的划分和关联,构造出一个完整的有向图。
在自动化测试中,有如下几个关键词:
启动测试、监听、主App连接、守护App连接、接口调用、进入后台、进入前台、应用退出、崩溃、断开连接、重连等。
在日常开发中,如果遇到上面的”事件”,可能就顺其自然的开始写判断、写调用,可能不自觉的就写出了一个“有限状态机”,不过不会那么严格的区分什么是动作什么是状态,只要满足最后的结果就能达成目的。
但现在我们有意识的利用有限状态机进行划分,分离出状态和动作以及状态迁移的条件。看上面的关键字,好像都是一个个“动作”,仔细看“监听(中)”又可能是一个状态,但实际上我们还得需要结合业务的理解再抽象出一些状态,如“进入后台”,则是跳转到了守护App,当前是控制守护App的状态;若是“进入前台”则守护App跳转到了“主App”,是控制主App的状态。
如下图就用刚才抽象出的关键词构造了一个简单的有限状态机:
按图说明:
如架构图描述的,需要主App和守护App同时连接才可执行测试
在连接完成后,状态直接迁移到等待测试指令的状态,没有任何动作
有些组合状态可以合成一个状态,如运行守护App状态时可能主App断开连接,也可能保持连接,所以区分为两态分别管理
当自动化测试框架启动后,除了监听两个App同时连接,其他状态都是在已有App连接完成的前提下进行的,所以大部分时间是在执行测试case调用及App切换的。