做者:【友盟+】高级无线开发工程师 吴玉强、王飞java
为了确保 SDK 线上运行的稳定性,咱们须要在开发后进行 SDK 测试,而为了提升测试效率,并且在拓展新项目的同时能兼顾已有项目的稳定性,在有限的资源内解放测试人员到更紧急的项目中来,就须要一个自动化工具来完成工做,【友盟+】独创自动化工具,可以自动传不一样参数、抓取输出数据,并自动验证数据准确性,输出结果,保障项目顺利稳定发布。python
相对 App 的测试方案,市面上已经有很是多且成熟的 UI 级别的自动化测试框架,却鲜有针对 SDK 提供的自动化测试方案,缘由是 SDK 属于为 App 提供服务的“插件”。一个 App 可接入一到多个 SDK 在内,而在项目中模块化是很是广泛的架构,因此 SDK 是针对细分功能提供服务的组件,有的提供数据服务、地图服务或节省开发成本的组件等等,这只能 SDK 开发者根据功能自行完成测试。shell
本篇说明的 SDK 测试方案是针对数据服务的 SDK 功能覆盖,皆包含 SDK 的 API、网络数据及缓存相关的逻辑测试,即非UI的纯数据逻辑的覆盖。json
本篇是自动化测试基础上的延伸,相对安卓系统能够便利的经过 adb 指令控制如App安装、卸载、退出应用等“系统”级操做,iOS 在控制 App 层面上只能经过一些间接的手段完成上面几点需求,为了易于维护,在控制器中以有限状态机模式进行了构造,以便于后续增长更多的操做状态和测试用例。数组
整个测试流程就以下面描述的有向图,以 Pytest 驱动客户端执行任务,而后将客户端输出的请求数据进行截取处理,然后验证是否经过测试用例。缓存
Android可使用adb命令与app进行数据上的通讯,如发送广播,启动Activity等。同时也可使用shell命令对配置文件进行修改,再进行gradle编译,实现对app级别参数的修改,从而完成不一样参数对app程序影响的验证。网络
iOS因为系统特性,没法如安卓系统灵活运用系统命令来操做 App 或 SDK,因此以一个 Socket 链接 Server 端进行通讯。
另外在 iOS 系统上又可利用 Runtime 的特性,将传输的字符串转化为 API 调用,这样作的好处是将 Socket 模块和 Runtime 解析模块编入应用中就无需再次打包,只需 Python 端编好代码和测试 case,全部的功能调用都由两端约定的协议解析执行便可。架构
对于集成SDK的app,若是须要在App运行时,触发一个行为,能够经过广播来实现。能够根据action name完成对行为类型的分类,根据caseid完成对行为的区分。以下图所示:app
根据上图示例以下:框架
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传递内容。
若是使用广播,是没办法绑定生命周期,即若是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进行相应的操做。
以上说的两种方式几乎能够涵盖SDK测试的部分case,可是对于部分SDK,初始化须要在程序一启动的Application中执行,这时上面的两种方式显然知足不了需求。
这时有两套方案能够应对。以下图所示:
二次编译
如上图所示,左边的部分,咱们能够经过修改Java文件完成对Appliction中内容的修改,如在Application中会有一些静态常量,使用python修改java文件中的常量,并从新运行:
def changeConstant(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 if os.path.exists(gradle_path): build_file = open(gradle_path, 'r+') lines = build_file.readlines() for i in range(len(lines)): line = lines[i] if ' '+source in line: 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
使用这种方式的好处是:
• 能够直接修改Application中的常量,如AppKey等,不用管是否执行了Application的onCreate()
• 不用考虑外设状况
• 一样适配对AndroidManifest.xml的测试
缺点是:
• 须要绑定工程路径
• 文件内容类型较多,容易出错,代码不具有通用性,有必定的二次开发难度
• 需使用gradle从新编译,如工程较大,耗时较长
配置文件
除了上述方法,也能够在Application中读取一个SD卡配置文件,根据配置文件的协议进行对应的操做。每次只需更改配置文件的内容,并经过adb push放入SD卡指定路径中,而后重启App便可。
这样作的好处是:
• 配置文件的协议能够随意定义,更灵活
• 配置文件可使用json格式,修改更简单
• 只需推到SD卡,耗时更少
• 不须要绑定工程路径
缺点是:
• 只能在Application的onCreate以后进行,局限性较大。
• 依赖外设SD卡
• AndroidManifest的测试没法使用。
如「iOS端测试框架」所见,此时进行通讯只有一个应用,这个应用就是咱们用来测试 SDK 的 Demo,经过这个宿主咱们能够触发 SDK 提供的任何 API,经过 iOS runtime 咱们能够触发 SDK 的类方法、实例方法甚至是私有 API,但这写都只局限于一个应用“沙盒”内,如上面说到的安装、卸载及 App 退出和切到后台就无能为力了,因此咱们引入了另外一个 Demo(Watch Demo),经过两个 Demo 的协同操做知足“沙盒”以外的需求。
两个 App 互相唤醒和通讯
如上面提到的,全部功能调用都基于约定的协议来执行的,协议的设计也是不断新增的测试需求改造的。
最初 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 字段,由于它是被动触发的,不在执行队列以内。
在这个业务协议开发过程当中,不断的根据测试需求进行改造、添加,从一开始的单一应用调用接口,到后面的多应用切换、先后台切换以及应用断开和重连,须要多套控制流程,在具体实现时,分散到了各个业务逻辑中,每增长一个控制都要兼容考虑是否会影响到其余模块,并且做为一个自动化测试“框架”,提早梳理好核心部分的流程会让以后更易于开发和维护,因此就引入了有限状态机的概念进行构造。
有限状态机(Finite-state machine)可用于模拟不少事物逻辑,顾名思义,它是一个有限的状态的处理逻辑,有下面几个特征:
状态数是有限的
在当前时刻只有一种状态存在
一个状态在知足某个条件后会切换到另外一状态
而有限状态机总体能够概括为四个要素:现态、条件、动做以及次态。
现态指当前时刻所表现的状态
条件又称为事件,即当前状态在知足这个条件后会触发一个动做,从而进行状态装换
动做即在现态知足条件后需触发的一系列操做,动做完成后即状态进行迁移。动做也能够忽略,在某些状况下,现态知足条件后,也无需执行任何动做就切换到新的状态。
次态是相对现态而言,表示了条件知足后迁移的状态,次态也能够与现态相同。
根据业务逻辑的特性及复杂程度,合适的使用有限状态机,可使得逻辑表达清晰、封装及维护都很直观和方便。当一个业务包含的状态越多,就越适合使用优先状态机进行封装处理。
有限状态机应用很是普遍,如电子电路、编译器及网络协议 TCP 协议状态机等
须要注意的是要区分“动做”和“状态”,若是将“动做”也视为“状态”会致使编写状态机时产生问题。
将业务逻辑应用到有限状态机,前提是须要熟悉对应的业务,并将其中的状态、动做和条件等抽离出来,而后再作进一步的划分和关联,构造出一个完整的有向图。
在自动化测试中,有以下几个关键词:
启动测试、监听、主App链接、守护App链接、接口调用、进入后台、进入前台、应用退出、崩溃、断开链接、重连等。
在平常开发中,若是遇到上面的”事件”,可能就顺其天然的开始写判断、写调用,可能不自觉的就写出了一个“有限状态机”,不过不会那么严格的区分什么是动做什么是状态,只要知足最后的结果就能达成目的。
但如今咱们有意识的利用有限状态机进行划分,分离出状态和动做以及状态迁移的条件。看上面的关键字,好像都是一个个“动做”,仔细看“监听(中)”又多是一个状态,但实际上咱们还得须要结合业务的理解再抽象出一些状态,如“进入后台”,则是跳转到了守护 App,当前是控制守护 App 的状态;如果“进入前台”则守护 App 跳转到了“主App”,是控制主 App 的状态。
以下图就用刚才抽象出的关键词构造了一个简单的有限状态机:
按图说明:
一、如架构图描述的,须要主App和守护App同时链接才可执行测试二、在链接完成后,状态直接迁移到等待测试指令的状态,没有任何动做三、有些组合状态能够合成一个状态,如运行守护App状态时可能主App断开链接,也可能保持链接,因此区分为两态分别管理。四、当自动化测试框架启动后,除了监听两个App同时链接,其余状态都是在已有App链接完成的前提下进行的,因此大部分时间是在执行测试case调用及App切换的。