相对 App 的测试方案,市面上已经有非常多且成熟的 UI 级别的自动化测试框架,却鲜有针对 SDK 提供的自动化测试方案,原因是 SDK 属于为 App 提供服务的“插件”。一个 App 可接入一到多个 SDK 在内,而在项目中模块化是非常普遍的架构,所以 SDK 是针对细分功能提供服务的组件,有的提供数据服务、地图服务或节省开发成本的组件等等,这只能 SDK 开发者根据功能自行完成测试。
本篇说明的 SDK 测试方案是针对数据服务的 SDK 功能覆盖,皆包含 SDK 的 API、网络数据及缓存相关的逻辑测试,即非 UI 的纯数据逻辑的覆盖。
本篇是自动化测试基础上的延伸,相对安卓系统可以便利的通过 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 完成对行为的区分。如下图所示:
根据上图示例如下:
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 数,相当于图中的 caseides 为参数内容,参数的协议可以自由定义,建议使用 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 中读取一个 SD 卡配置文件,根据配置文件的协议进行对应的操作。每次只需更改配置文件的内容,并通过 adb push 放入 SD 卡指定路径中,然后重启 App 即可。
如「iOS 端测试框架」所见,此时进行通信只有一个应用,这个应用就是我们用来测试 SDK 的 Demo,通过这个宿主我们可以触发 SDK 提供的任何 API,通过 iOS runtime 我们可以触发 SDK 的类方法、实例方法甚至是私有 API,但这写都只局限于一个应用“沙盒”内,如上面说到的安装、卸载及 App 退出和切到后台就无能为力了,所以我们引入了另一个 Demo(Watch Demo),通过两个 Demo 的协同操作满足“沙盒”之外的需求。
如上面提到的,所有功能调用都基于约定的协议来执行的,协议的设计也是不断新增的测试需求改造的。
最初 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 的状态。
如下图就用刚才抽象出的关键词构造了一个简单的有限状态机:
原文转自:http://www.infoq.com/cn/articles/sdk-test-automation