首页 > 代码库 > Mock+Proxy在SDK项目的自动化测试实战
Mock+Proxy在SDK项目的自动化测试实战
项目背景
广告SDK项目是为应用程序APP开发人员提供移动广告平台接入的API程序集合,其形态就是一个植入宿主APP的jar包。提供的功能主要有以下几点:
- 为APP请求广告内容
- 用户行为打点
- 错误日志打点
- 反作弊
团队现状
在项目推进的过程中,逐渐暴露了一些问题:
1. 项目团队分为上海团队(服务端)和北京团队(客户端),由于信息同步,人力资源等其他原因,服务端与客户端的开发进度很难保持同步,经常出现客户端等着和服务端联调的情况
2. 接口文档不稳定,理解有偏差
3. 协议变化频繁,消息不同步
4. 缺少服务端测试环境,可模拟的真实广告内容太少
5. 协议字段太多,传统的测试用例设计方法容易出现遗漏,尤其是异常情况处理,测试完成以后测试人员对字段覆盖率没有信心
协议字段示例图
{
"ads": [{
"action": { "path": "" },
"adw":920,
"adh":900,
"template": "",
"action_type": 2,
"adid": "67346778",
"adm": "",
"adm_type": 0,
"deep_link": "",
"impid": "nXcM_kqBGqL=",
"tk_act": [""],
"tk_imp":[ ""],
"tk_ad_close": [""],
"tk_clk": [""],
"tk_dl_begin": ["" ],
"tk_dl_btn": [ ""],
"tk_dl_done": [""],
"tk_dp_suc": [],
"tk_ins": [ ""],
"tk_open": [""]
}],
"errno": "0"
}
- 测试用例设计极容易受需求影响,更新起来非常麻烦,成本很高
- 手工测试方法执行效率低,且容易漏测
手动测试过程示意图
分析&思路
上述几个问题,其中1、2、3都会对我们的测试工作产生影响,但是属于项目管理范畴,不在本文讨论范围内。那么针对4、5、6、7几个问题,应该如何解决呢?
首先分析一下问题:
Q: 缺少服务端测试环境,可模拟的真实广告内容太少
A: 由于服务端团队在人力上的不足,无法为我们提供测试环境,通过沟通协商,方法暂定为由服务端同事预先配置好线上广告物料,即固定的线上广告资源,能够覆盖提测的广告类型,在服务端完成功能逻辑之前,先利用mock方式测试客户端的功能逻辑以及展示,此时客户端和服务端后台无需交互。Q: 协议字段太多,传统的测试用例设计方法容易出现遗漏,尤其是异常情况处理
A: 制定一个可靠的测试用例设计策略,以最少的case覆盖最多的情况。Q:测试用例设计极容易受需求影响,变更起来非常麻烦,成本很高
A: 对测试用例进行拆分,分为正常返回情况和异常处理两部分。正常的处理包括系统环境、网络切换、下载、轮播、缓存、正常打点、安装卸载、UI检查等等需要人工检查的情况,因此这部分我们先梳理checklist,先组内review,再约产品和研发一起review,确保需求的完整性,另外开发过程中的需求变更是不可避免的,对于需求的变化要做到实时更新case,这部分case覆盖的点要足够全,而文字描述要尽量的精简,确保更新起来能快速响应节奏的变化。而异常的部分,我们的做法是批量自动化生成case,生成策略会在下面详细描述。Q:手工测试方法效率低,且容易漏测
A: 正常的功能我们通过手工测试的方法覆盖,而对于客户端拿到的异常情况的error code要有全量的覆盖,比如我们的错误代码约定了21种,那么针对所有可能出现的错误代码都要想办法触发,这一部分工作希望从case生成到用例执行能100%的自动化实现。
调研过程
有了解决思路,那么需要想办法把想法落地。我们提炼出几个需要攻克的技术难点:
难点一:mock框架选型
做过单元测试的同学应该了解“桩(stub)测试”,即通过hard code方式验证函数的输入输出是否准确和健壮,而mock测试和桩测试类似,功能要更加丰富一些,可以模拟产品交互环节中的部分场景,换句话说,可以让测试工作提前介入研发流程中。多用于需要联调的环节,比如支付场景,购买流程,第三方插件调用等等业务。之前我们采用的Fiddler重定向请求结果到本地文件的方式模拟服务端的response来欺骗客户端,也可以理解为mock测试。
最初我们计划自己写一个proxy server监听指定端口,截取所有的http/https请求,再替换response内容完成mock测试,后来一次偶然的机会接触了阿里开源出来的anyproxy(http://anyproxy.io/cn/),了解了一下该工具,发现这款工具刚好满足了我们的几个需求:- 代码开源
- 规则可定制
- 支持https可视化
- 易部署、学习成本低
- UI可视化,类似fiddler
实际使用截图(我们对response展示做了点优化):
难点二:可靠的测试用例设计策略
在讨论接口测试用例设计之前,我们需要预先圈定一个思考范围,以免过度的思维发散。结合我们的业务特征,由于SDK的功能大部分是单接口,少部分是关联接口,因此我们的设计基于单接口而非单个业务场景
。
接口的测试用例设计有别于其他测试用例,其业务逻辑主要体现在字段的取值上,每个取值体现了一种业务逻辑,我们做了一些调研,学习了其他业务团队的接口测试用例写法,发现测试人员喜欢这样设计case:
这样的case无疑是工整、直观的,可读性比较强,很方便的复制粘贴,再通过修改其中的一个或者几个值,形成一个庞大的二维数组。
看到这个表格,一些熟练的测试工程师会立马联想到边界值、等价类设计、正交试验法等。然而要想保证每一个场景都被完整的覆盖,理论上我们需要测试所有字段的笛卡尔积,这种方式可以保证任何取值都会被覆盖到,但是当字段比较多的时候,测试用例的数量会呈爆炸式的增长,毫无疑问这种方式是不可行的。我们需要一个算法,能做到以下几点:
1、 以最少的组合覆盖尽可能多的场景
2 、覆盖所有字段的所有取值
3 、有统计学支撑,生成的数据有规律可循
有了需求,我们开始进行了可行性方案的研究,秉承不重复造轮子的理念,我们查阅了国内外很多的资料,逐渐的缩小了范围,在说出解决方案之前,先给大家简单介绍两个重要的算法:“ OATS(Orthogonal Array Testing Strategy)”和“Pairwise/All-Pairs Testing”,简称“正交表法”和“配对测试法”。
正交表法
正交表法有两个重要的特性,大家尝试着理解一下:
1.每列中不同数字出现的次数相等
备注:这一特点表明每个因素的每个水平与其它因素的每个水平参与试验的几率是完全相同的,从而保证了在各个水平中最大限度地排除了其它因素水平的干扰,能有效地比较试验结果并找出最优的试验条件。
2.在任意两列其横向组成的数字对中,每种数字对出现的次数相等
备注:这个特点保证了试验点均匀地分散在因素与水平的完全组合之中,因此具有很强的代表性。
举个例子:有三个字段,每个字段可以取三个值,设字段表现为A(A1,A2,A3)、B(B1,B2,B3)、C(C1,C2,C3),可以组成的集合恰好可以表现为一个三维空间图,如下图所示:
图中的正方体中每个字段的每个水平代表的是一个面,共九个面,任意两个字段的水平之间都存在交点,共27(3x3x3)个,这就是笛卡尔积。按照两大特性设计出的正交表如右图所示,试验点用⊙表示。我们看到,在9个平面中每个平面上都恰好有三个点而每个平面的每行每列都有一个点,而且只有一个点,总共九个点。这样的试验方案,试验点的分布很均匀,试验次数也不多。
国外有一个网站能查询正交表的结果案例:http://www.york.ac.uk/depts/maths/tables/orthogonal.htm
配对测试法
配对测试法(Pairwise)是L. L. Thurstone( 1887 – 1955)在1927年首先提出来的。他是美国的一位心理统计学家。Pairwise是基于数学统计和对传统的正交分析法进行优化后得到的产物。
定义:Most field faults were caused by either incorrect single values or by an interaction of pairs of values.” If that’s generally correct, we ought to focus our testing on the risk of single-mode and double-mode faults. We can get excellent coverage by choosing tests such that 1) each state of each variable is tested, and 2) each variable in each of its states is tested in a pair with every other variable in each of its states. This is called pairwise testing or all-pairs testing.
大概意思是:缺陷往往是由一个参数或两个参数的组合所导致的,那么我们选择比较好的测试组合的原则就是:
1)每个因子的水平值都能被测试到;
2)任意两个因子的各个水平值组合都能被测试到,这就叫配对测试法。
参看:http://www.developsense.com/pairwiseTesting.html
Pairwise基于如下2个假设:
1. 每一个维度都是正交的,即每一个维度互相都没有交集。
2. 根据数学统计分析,73%的缺陷(单因子是35%,双因子是38%)是由单因子或2个因子相互作用产生的。19%的缺陷是由3个因子相互作用产生的。
因此,pairwise基于覆盖所有2因子的交互作用产生的用例集合性价比最高而产生的。国外也有一份类似的数学统计:
我们通过一个订飞机票的实际例子来看一下,配对测试法是怎样从笛卡尔积中提炼出局部最优解的。
依然是三个字段的组合,分别是Destination(Canada, Mexico, USA),Class(Coach, Business Class, First Class), Seat Preference(Aisle, Window),所对应的笛卡尔积共有3x3x2=18中测试组合,如下表所示。
经过配对测试法筛选后,结果如下:
经过筛选以后,我们的测试用例变成了9条,case数量精简了50%。简单总结pairwise的筛选原理就是,发现两两配对在全集中有重复的就去掉其中之一,这样筛选也有副作用,每次筛选完了条数是固定的,但是结果却不尽相同。但是通过上面的介绍我们不难比较出两种算法的差异。
说了那么多,再回到我们之前提到的设计策略几个需求,可以认为pairwise算法的特征基本满足了我们的需求。
- 难点三:测试用例自动化生成
确定了用例设计的算法策略后,我们信心十足的准备开始设计我们的response返回值case了,我们套用文献中的排列分布方式应用到实际接口json中,悲伤的发现我们要组合的字段不是3个,而是20-35个左右,如果通过人工的方式来进行case设计的话,就算只考虑最多两个字段的值发生变化,数量也是非常惊人的,WWWWWWhat???
本着“偷懒是人类进步的第一动力”的想法,我们自然不会前功尽弃,自动化测试是我们的必选之路,接下来要做的就是调研目前已经存在的基于pairwise算法的工具有哪些,下面是经过调研后得到的工具列表。
基于pairwise算法的工具如此之多,那么相同模型设定下产生的结果是否存在差异呢?我们看一下这张图:
综合比较各工具产生的数据结果后,我们可以发现不同工具之间的结果差异并不大,基本上能够满足我们现有的需求。经过一番讨论后,我们决定采用微软的PICT(https://github.com/Microsoft/pict)作为case生成工具,原因有几点:
1. 代码开源可扩展
2. 源码依然在维护,贡献比较活跃
3. 产品成熟,语法丰富
4. 基于贪心算法,局部最优解
难点四:测试用例生成的设计
用例生成过程分为五个步骤:
1. 准备字段值
根据Wiki的接口文档,测试人员理清字段结构,字段类型,字段取值范围后,结合传统的case设计理念,构造出每个字段的赋值,存放到整理好的excel中,大概是这样的:
有的同学可能会问:你这样整理也挺麻烦,感觉人工也没省多少事儿。这样设计的好处是,当字段发生变化的时候,只需要从源头修改字段属性、值、层级、甚至删除,后面整个流程中的case都会统一生效,字段集中管理,牵一发而动全身。和UI自动化用到的page-object设计类似。
2. 构建模型
有了面粉了,还需要加工一下才能变成我们想要的面包,我们需要把准备的数据整理成可以批量生成的可识别文件,即模型文件。PICT的模型文件有自己的格式,类似这样:
参数定义
[子模型定义]
[约束定义]
举个例子,前面提到的订票系统的例子加工成模型文件是这样的,后面会给大家介绍语法含义:
Destination: Canada, Mexico, USA
Class: Coach, Business Class, First Class
Seat Preference: Aisle, Window
{Destination, Class} @ 2
3. 生成Case
通过pairwise工具将模型文件组装成我们想要的case,那么上面的模型生成的case会是这样:
Destination | Class | Seat Preference |
---|---|---|
Canada | First Class | Window |
USA | First Class | Aisle |
Canada | Coach | Aisle |
USA | Business Class | Window |
Mexico | Business Class | Aisle |
Canada | Business Class | Aisle |
Mexico | First Class | Window |
USA | Coach | Window |
Mexico | Coach | Window |
注:选择强度为2,因此上面的矩阵是两两变化的。如前面所说,这里生成的矩阵内容不是固定的!
4. 准备期望结果
输入数据已经准备好了,那么相对于case而言,是不是还缺一个期望结果呢?在这里我们碰到了一个难题,可能做过case自动生成的同学都会遇到的,就是生成排列组合是非常简单的,如何让这些组合变得有意义,体现在我们的期望结果上,那么一次性生成如此多的case,如何让输入值和期望结果对号入座呢?
我们的做法是:拆分了postive testing 和 negative testing(合法输入测试和非法输入测试或负面测试),通过整理接口case我们不难发现,合法输入的case其实占整个case的比重并不大,工作量比较大的是各种参数的异常数据输入,相应的会产生error code或二次请求。只需要我们在整理数据的时候给出对应的error code即可,如图所示:
有的同学会问:我们协议还不稳定,error code也不明确,有些输入也不知道对应什么error code,怎么破?别急,后面告诉大家。
5. 生成mock数据
完成了以上准备工作以后,剩下的就是生成我们mock需要的response json数据了。解析Wiki协议中的json模版,给对应的json字段赋上生成的值,这里需要写一段代码来完成,在此不做赘述。
番外篇:工具的二次开发
在使用过程中,我们发现工具PICT不能满足业务场景的复杂度要求,主要有两点:
- 异常输入测试的时候,不能同时输入多个异常值
在case设计中多个异常值输入是很常见的测试场景,虽然pict提供负面测试(negative testing)功能,即如果模型文件中,有值被标记为异常值(默认的异常值标识符为“~”),则case中会随机出现一个异常输入的值,但是PICT限制每个case只能有一个异常值存在,原因是多数异常值的组合虽然可能会引发问题,但是代码在catch了一个异常值造成的异常后,不会再去处理另一个异常值。
先通过一个示例来感受一下pict的负面测试。示例模型文件如下:
Destination: Canada, Mexico, USA, ~Japan
Class: Coach, Business Class, First Class
Seat Preference: Aisle, Window, ~Door
产生的case如下:
通过上图可以看出,PICT同时保证了正常值的组合,也保证了异常值的组合,但是我们不难发现,每个case只会出现一个异常值,那么 ~Japan,First Class, ~Door
的case就会遗漏,显然case覆盖率不够,不能满足我们的需求。
针对这个问题,在对PICT的源代码进行了详细的解读后,我们对代码进行了二次开发,扩展了负面测试的覆盖范围,彻底解决了这个问题,修改后的模型文件如下:
Destination: Canada, Mexico, USA, ~Japan
Class: Coach, Business Class, First Class
Seat Preference: Aisle, Window, ~Door
~{Destination, Seat Preference}
//增加一行公式,在模型文件中指定了Destination和Seat Preference两个字段可以进行异常值组合,数量不限。
扩展后的case生成是这样的:
- 正则表达式过于简单,不支持复杂的语句
PICT支持IF[ ] THEN[ ]格式的约束规则。但是约束规则中LIKE关键字的通配符操作只支持*和?(分别表示任意多个字符和任意一个字符)。显然简单的通配符操作限制了约束规则的表达能力。因此,我们在原有的基础上,引入C++的regex库支持正则表达式,修改后支持了更丰富的正则表达式。
如下示例中,增加一条规则,如果Destination字段为数字类型(”\d”),那么Seat Preference字段也为数字类型。
Destination: Canada, Mexico, USA, 3
Class: Coach, Business Class, First Class
Seat Preference: Aisle, Window, 4
If [Destination] like "\d" then [Seat Preference] like "\d";
生成的case如下图:
有了强大的正则表达式,再加上多异常组合输入的支持,目前已经完全能覆盖我们需要的任何场景,向开源致敬!
结构流程设计
在整个测试过程中,我们唯一需要人工介入的就是字段值的赋值以及跟error code的对应关系设计,协议字段的取值会受业务影响,暂时无法通过自动化的方式来进行。流程如图所示:
最初的版本比较简单,结构大概是这样的:
原本的设想是想绕过广告宿主直接调用SDK的API请求广告,从而节省一部分时间,且更容易自动化,但是由于广告SDK本身特殊的设计,这个想法无法实现,因此当时的设计是通过触发APP按钮点击发送request给SDK,再由SDK发送加工后的请求到proxy server,再经过mock server处理数据以后,返回给客户端来显示广告。在此过程中,彻底抛弃了Fiddler重定向的传统做法。
在阶段二我们解决了几个问题:
- 实现内部循环请求广告,解决手工触发请求的问题
- 监控APP自身出现的crash和ANR
- 解决case失败后可以rerun,
- 解决中途执行中断可以rerun
- 由于一些广告请求失败会触发二次打点请求,因此我们需要把对应的case和打点请求结果匹配上,我们通过在request中加入caseID来解决该问题
- 解决多种广告类型不能连续一次性运行完,需要切换场景的问题
- 当出现期望结果与实际结果不符时,自动重新运行该case 若干次(可配置),如果一直失败计为case失败
该阶段很明显,我们遇到了执行速度的问题,由于广告种类的增加,我们的case达到了3400余条,由于还需要兼顾广告渲染完成后的打点结果检查,执行全量case耗时达到了3个小时多,偏离了我们mock测试的初衷。因此如上图所示,我们用到了分布式结构。mock server可以通过客户端指纹信息来调度和发送任务给指定的手机,把case和设备紧密连接在一起,避免重复运行相同的case。
另外我们把config、case、期望结果、执行结果等诸多信息全部迁移到database中,一方面解决频繁的文件读取问题,另一方面解决了分布式调度跨server的问题。
截止目前,我们的测试数据是这样的:
Case总数 | 发现BUG | 遗漏的异常处理 |
---|---|---|
3498条 | 77个 | 331个 |
前面提到的问题,如果error code尚未明确,case应该如何匹配呢?我们的做法是设定一个基准error code,当运行结果出来后,会有实际结果与期望结果不符的case,拿去和开发对一下就可以,而调整error code期望结果以后,重新生成case也只不过分分钟的事。
我们的执行时间:
收益:协议变更时,只需要修改最开始的存放字段值的文件,后续的建模、case生成、期望结果填充、执行测试用例全部自动完成,测试人员查看运行结果即可
问题总结
由于我们也是第一次在mock测试中实践自动化构造测试数据,包括用到的pairwise模型的合理性和准确性,都属于初次尝试,目前在项目中取得了一定的效果,但是也遇到了很多的困难,个中酸楚不足以一一道来,同时架构和流程还有很多优化空间。
目前依然留存的问题包括:
- 自动生成case中,int、string、date等字段提取公共case,比如特殊字符、空、null、js等常规异常检查
- 更复杂的逻辑,比如关联字段依赖、加密字段、随机数、MD5、token等情况
- 非http(s)的自定义协议
- 分布式调度的更大规模的使用
- SDK的自动化测试对于APP的强依赖关系
- 正常的功能测试验证
- 业务逻辑产生的漏测率统计
诸如此类的问题还有很多很多,尤其是结合项目自身特点,就会更加复杂,希望能通过我们的探索之路给同行们更多的启发。
Mock+Proxy在SDK项目的自动化测试实战