相对 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 数,相当于图中的 caseides 为参数内容,参数的协议可以自由定义,建议使用 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 文件中的常量,并重新运行:
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 自动化测试流程
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 切换的。
感谢徐川对本文的审校。
评论