Monkey测试的进化之路

首先,这不是一篇技术文档,是一篇对Monkey测试及上下游的总结。

来字节跳动后,由于工作关系跟Quaility Lab部门的同学接触了也一年多了,一直在享受别人在稳定性测试工具上的研发成果,虽然没办法偷师到别人的源码实现,但是在思想上还是可以get到一点的,再融入一些业务方或者用户方的视角,这里就用一篇简单的文章来记录我对Monkey测试上下游的理解。


Why Monkey

Monkey测试作为一种手工回归的补充或者测试的兜底手段,肯定是必要的,大家对它基本预期是:

在有限的时间内,通过基础点击动作,自动遍历尽可能多的app页面,从而一定程度上达成补充或替代手工集成测试的目的。

引入Monkey测试目的就是降低研发成本,提高产品质量。Monkey测试能拿到的结果,人肉一样能拿到(本质上自动化能做的东西,肯定得先有人肉+工具实现的案例)。同时无数的研究报告也表明了,Bug越早被发现,修复成本越低,所以在app新版本的生命周期开始那一刻,Monkey就要全功率跑起来,用线下低廉的设备成本和修复成本来换取线上宝贵的用户体验和口碑。

对Monkey的预期中,特地用逗号隔开几个基本前提,Monkey测试的最基本评价点分别是“时间成本”和“测试覆盖”,而“测试覆盖”又会细分出更多的评价维度,暂时不做展开。


How to benefit from Monkey

上图是本文梗概,因为这里我们讨论的对象是Monkey,所以出发点是Monkey测试工具。实际上它的父话题是测试效能,而Monkey测试仅仅是测试效能中一个点而已。

那如何利用Monkey测试工具获得我们想要的收益呢?

第一点,可以在不同的研发流程环节引入,集成到流水线中发挥测试兜底作用,降低人力成本。

研发流程规不规范都好,总归会先有需求和UI设计,然后是实现方案设计,再来是代码实现和测试设计并行,最后是测试、修复以及PM验收等环节。这里可以划分出几个不同的阶段或者几种动作,举例:

  • 按阶段划分:需求/开发阶段 -> 测试阶段 -> 回归阶段 -> 灰度阶段 -> 线上阶段
  • 按动作划分:测试包构建 -> BugFix包构建 -> 回归包构建 -> 灰度包构建 -> 线上包构建

那就意味着,在不同的阶段/动作,我们可以具备不同的测试预期,从而以不同的形式/投入来使用Monkey。举两个例子:

  • 按阶段划分的策略,回归阶段可以投入最大的成本去做Monkey集成测试,全局发现尽可能多的问题;而在测试阶段,因为app本身还不稳定,可以通过减少Monkey测试时长或者测试机器数来降低测试成本,是需要把关最基本的启动和核心场景不易现崩溃即可,剩余由QA手工测试把控。
  • 按动作划分的策略,实际一些动作可能会贯穿不同的阶段,比如BugFix这种东西,它可以在回归、灰度、线上阶段去合入,那针对BugFix应该有一些特别的策略,比如RD都希望只测到我的BugFix相关功能,别人的功能我不管。

第二点,研发流程的打通,往DevOps概念靠拢。

测试效能总在说DevOps,实际是在大家平时接触的事物里抽取基本概念再泛化一下。如果要跟研发流水线结合,比较容易想到的是Monkey测试发现的问题怎么消费,现阶段Monkey主要还是用来发现app的崩溃问题,这里会涉及崩溃上报、问题提单、问题统计等一系列后续消费事宜,不同公司的链路不一样,基本思路是:

  • 问题发现 -> 问题上报 -> 问题过滤 -> 问题分配 -> 问题定级 -> 问题提Bug -> 问题排查 -> 代码合入 -> 再来一遍

这样下来,问题生命周期流转环节都给它安排得明明白白,只要消费能力跟得上,丢问题、漏问题的概率就会更低。如果消费能力跟不上,就砍“问题发现”的能力,或提高“问题过滤”和“问题定级”的门槛,从而降低最后到达RD手头的问题数量。

如果条件成熟,还可以不同程度上实现测试发布的准入准出(或基线红线)的作用。

第三点,通过线下规模化部署测试(堆成本),暴露极端问题,或提高目标问题的复现概率。

50分做到70分容易,90分做到95分很难,道理大家都懂。要做到更极致,也不是必然要堆成本来实现,但如果希望控制时间,要更快,那并行就是最直观的出路,当然还可以引入专项故障注入能力来提高极端问题的暴露概率。

要修复什么问题,这个标准往往是拿捏在RD手上,除非QA有坚挺的线上数据作为理论支撑。也不是什么问题都要修复(实际上或许也没人力去修复,让日理万机的RD去修复一个线上只出现一例的问题确实难说通),尤其是在人力不充足,app迭代快的业务背景下。

所以,这一点更多是锦上添花的作用,比如线上有个高频问题,由于环境特殊不好排查,那么线下能否模拟出线上用户的环境来复现问题?或者让RD做修复后在这个环境中验证问题?或者为了追求极致的体验,app能否在高压环境下正常使用?这些问题大概率就不是业务要解决的核心问题,而是业务增长或者技术上的追求极致,可以做适当取舍。


Monkey测试的评价标准

比较容易想到的指标,无非下面这些:

  • 覆盖率:app页面覆盖率 & 代码覆盖率 & Diff页面/代码覆盖率
  • 测试耗时:测试任务耗时 & 总机器测试时长
  • 问题发现数(召回数)、被修复问题数

三者的关系,覆盖率是Monkey能力的技术指标、问题发现数是业务收益指标,测试耗时需要前两者搭配一起食用才具备意义。

这里的评价指标看着比较简单,对于Monkey来说,背后需要挖掘的方向其实多得很,下面一节详细说明。


Monkey测试能力的理解

如果换一个角度来理解Monkey测试能力,往最纯粹的要素来看,Monkey工具它本身就是一个纯粹的 “UI遍历驱动器” ,它实现了自动化点点点的能力,它让app工作起来(也不是说不操作app,app就不工作,毕竟还有后台任务),运行到不同的代码分支逻辑里头。那基于Monkey的 “遍历能力” ,还能否附加更多价值?当然可以!

测试策略

多机遍历

Moneky测试最核心的能力还是要回归到“遍历”上,那么如何多快好省地执行遍历就是最重要的课题。思路主要是三个:

  1. 增加测试机数量,并行遍历,更短时间达到同样的覆盖率 —— 后端的QPS、算法效率、Monkey客户端的性能损耗
  2. 单机测试时长延长,提高总的测试机测试时长 —— Monkey客户端的稳定性、功耗
  3. 不一样的遍历策略(优先覆盖率or测试压力),app手势操作策略,其他操作策略(app前后台切换、app退出等)

如果基于测试覆盖率来衡量效果,那多机遍历就是使得达到同等测试覆盖率的前提下,通过横向扩展测试资源来缩短测试总时长,从而在相同时间内可以触发更多次的测试。

定向/精准测试

改哪里测哪里,RD当然不希望在自己的合入里发现了问题,然后花时间一看原来是别人的问题。所以在原来的“遍历”上引入测试目标或测试方向,就是Monkey的另一个能力。关键问题就是如何实现“定向”。

QL团队的Fastbot已经做出了方案Fastbot自动化精准:改哪儿测哪儿 ,思路很直白,大体由两部分能力组合而成:

  • 自动寻路的能力 —— Fastbot 定向稳定性测试方案 基于历史访问UI链路,自动寻路到对应页面
  • 确定测试范围的能力 —— 基于静态代码分析,获得代码变更在一定范围内影响的方法代码,再通过这些方法映射到页面,从而推荐出哪些页面需要被测试

基于以上的方案,要实现代码合入时的定向测试是没问题的,至少可以控制Monkey在这些页面上停留更长时间做更多点击滑动等测试,去做到更高的覆盖率,结合代码Diff插桩,就可以获取到增量代码的覆盖率。

如果结合业务视角,希望将更多测试时间放在更重要的场景上,可以定时监控线上top热度的页面,线下保持Monkey在这些页面上的测试时长即可。

直观难点是静态代码分析环节,基于AST分析获取类之间的调用关系,理论上是收集不全的。以Java为例,基于配置文件通过反射动态生成的类,这种在AST角度应该无法追踪,它们对于AST来说就像凭空产生的幽灵,回溯不了出处(感觉增加人工语法规则辅助应该能一定程度追溯到),意味着本该存在的调用链就丢失了。

在获取到调用关系网状图后,即可按照人为规则,针对某个类我们说它上下游各多少层就算是存在关联关系。那拿到Code Diff,所改Code对应的类在网状图里一套,Diff Class就出来了。

类与Activity的映射关系获取手法就丰富一些,感觉上可以基于AST来获取,也可以基于运行时产生的代码覆盖信息计算出来。

拟人化测试

RD总是说,希望Monkey测试时要尽量模拟线上用户的操作,或者尽量在核心场景多测一会儿,这个想法是无可厚非的,要解决的问题也很明显:

  • 哪些页面在核心场景里
  • 什么操作才算是线上用户的操作

估计大家都不需要思考,就知道这些答案全都在线上用户数据里头隐藏着了,所以答案也很简单,直接拉取用户真实操作数据过来训练(当然要脱敏),收集如操作手势、访问路径、访问时长等数据,就可以做出一个近似线上真实用户的模型,从而在面对一些列控件树去做点击决策时,尽量模拟一个真实用户。以头条为例,做到启动app后,下滑刷新几次feed,下拉选择某篇文章点击进去,再慢慢划到评论区点赞评论等。

跨平台

无需多说,Android、iOS、Flutter、Webview、Web浏览器、Windows、Mac、Linux,不同的产品类型,都可以支持上。

对于后端和算法模型来说,区别应该不会很大,重点差异在客户端,因为不同平台在解析控件树、执行交互操作等方面上有天然的区别,技术无法复用。

能力集成

集成专项测试能力

有了Monkey的遍历能力,专项测试能力非常多,千奇百怪,较为常见的思路是参考服务端混沌工程,在客户端引入类似的能力,通过注入故障的形式来模拟极端环境,观察app的表现,如控制app进程内外的高CPU、高内存占用、IO频繁swap、IO带宽打满、app维度的弱网断网等通用资源的故障。

除了上述的“通用资源故障”外,更有意思的就是业务维度的典型问题,可以阅读文末参考资料的“随机Crash”的两个例子,还有一些常见的业务问题,有些想法但是不知道可行性:如加载时序问题,通过注入延迟,能否使得Android插件被延迟加载(这个肯定可行),或使得对象加锁延迟从而提高出现死锁的概率,或使得Dex文件延迟加载出现某功能未加载但是被使用的现象,更多想法可参考[建设构想] 客户端稳定性问题防控服务 ,后续会持续补充case,我们(移动专项测试服务中心)正在做相关建设与规划。

测试能力作为测试本身的主要卖点,建立生态,集思广益也是很重要的,要让用户能利用Monkey的遍历能力来开发新的测试能力,解决不同业务的问题,所以必须可扩展。

录制回放

其实除了测试出问题外,Monkey工具也可以作为线下自动回放的UI驱动工具,应用场景是在线上线下发生崩溃后,能通过app回传的信(或Monkey有某种滑动窗口机制来缓存一定时间之前的动作信息)息进行崩溃前操作与数据的回放,从而最大程度协助RD Debug,提高线下定位调试的效率。

关于录制回放,技术难点应该比较多,但是对方案没怎么做了解,所以这里没法帮大家做简单的总结,客户端录制时的数据上报协议以及客户端的回放引擎,都是难点,尤其是在不同分辨率的手机上做到精确回放,参考内部方案用户行为录制回放技术方案

Fuzz

Fuzz有很多种,国外和学术界已经搞了不少Android上的各种Fuzz方案,如 Intent Fuzz等(了解有限暂时枚举不了更多)。

排除app内部的Fuzz,在服务端响应层面上的Fuzz则更加常见,主要用来验证客户端接口的健壮性,面对服务端突如其来的“抽风response”(往往是接口协议变形)能否稳定地处理。测试目的是后手检验客户端是否有意识地做防御式编程,做容错判断。因为服务端真的可能随时有问题,就不按常理出牌,比如服务端响应的某个字段原本约定是不为空的,但是服务端抽风它就给客户端一个空,然后客户端啪地一下崩了,这种事故在头条也不是两次三次了。

这样听起来服务端响应的Fuzz也蛮好的,但在实操层面上要需要解决额外的问题:

  • Fuzz出客户端崩溃,要保留导致客户端崩溃的response上下文,从而有目的地Debug修复
  • Fuzz要更多测试核心接口,边边角角冷门接口可以不测试或适度测试

并不是十分困难的问题,但是一般需要多个平台之间打通,把能力当成一个产品来做,这样才能做好。

Fuzz落地会遇到两个关键问题:

  1. Fuzz发现的崩溃难以消费
  2. Fuzz测试的变异策略如何制定

问题一: 崩溃发现到消费总会有一个鸿沟,对于RD来说,这个崩溃首先要关联到请求上下文,除了崩溃堆栈外还要带上导致崩溃的响应。有些崩溃在客户端做json decode的时候就因为schema不匹配就直接crash了,这种问题比较明显;有些崩溃则在更深的地方暴露,json decode没问题,但在更深的代码,比如某个变量类型转换出现问题,此时堆栈就会很深,已经很难直观看出来是因为服务端返回不合类型的字段值导致的问题,这时如果不能直接关联到引发崩溃的具体reponse,则问题排查解决难度骤升。

另外,Fuzz如果不做到精准测试,那么它就只能是一个后置验证手段,更适合做摸底排查,难以实现测试左移前置拦截问题。

很多时候,希望验证某个需求是否ok,或更典型的,因为春节活动吃服务器资源要做低优接口降级,我用Fuzz能否验证那些被降级的接口降级后客户端是否正常。这样的背景驱动下,就需要Fuzz定向到某个场景某个接口某个字段工作,最好还能根据Diff自动Fuzz。简单的做法是基于黑白名单人工圈一个测试范围,但是成本高很麻烦,每次都要人给配置还存在人工失误的可能性。更好的办法是自动识别线上高访问量的接口以及频繁变动的字段,基于日志或流量来做都可以。而理想中更优的办法是黑白名单结合自动识别的混合策略,因为有时候某些接口虽然低频访问但很重要,比如登录接口、支付接口,这些接口最好能Fuzz到;有些接口虽然高频访问但缺斤少两也没关系,比如客户端日志上报、普通埋点上报,这些接口不Fuzz也没所谓。

问题二: 要回答这个问题,先从客户端接口Fuzz的目标来看,它要拦截的是服务端不按预期返回数据给客户端,客户端恰好没做校验最终崩溃的问题。这里有两个要素,分别是“发现问题”和“解决问题”,变异策略就是发现问题。

在实践过程中,变异策略不是越多越好,因为越多的变异策略代表更不聚焦的测试目标与更低效的问题发现。举个例子,服务端某个接口返回字段A,它是int类型,此时考虑是优先把它变异为空,还是把它变异为string?当然是选择变异为空,因为前者是更典型的“服务端抽风”现场,“全都要”是不现实的,因为测试时长有限,而且RD更倾向于认为int类型的字段被改为string的概率很低,更不愿意去修改这种问题。变异规则指定最好还是根据线上典型的异常response来提炼,基于实际问题来制定。

性能测试

性能测试具体怎么做就不再这里开展了,这里表达的点是可以利用Monkey的遍历能力,来实现大范围的性能数据采集。因为常规方案都是基于UI自动化的形式定向获取数据,优点是适合做场景的精确模拟,更加适合竞品之间的性能数据对比,缺点是数据采集的范围有限,扩大范围就要线性增加人力投入来新增和维护自动化case。

使用Monkey作为UI驱动器弥补了测试范围有限的缺点,它可以在线下实验室环境下大范围采集app的性能数据,这个时候可以不依赖于RD埋点上报来统计耗时,可采用更加贴近用户体验的是录屏分帧的形式来计算场景耗时(因为线上只能基于埋点统计,没有其他办法),缺点就是测试范围不好控制,同时引入Monkey工具本身对系统资源损耗多少的问题,从实践来看,还真不太好优化Monkey性能,因为频繁的控件树读取以及与后端算法模型的网络交互再到点击事件生成这个过程本身就需要较多资源。所以这一项也就只是一个猜想,没有实践过,ROI也不高。

安全合规

参考质量工程x安全:动态检测基础能力专项 ,还是基于Monkey的遍历能力,如果在系统环境上动手脚,比如我们将ROM坐一些魔改或者Hook,然后跑在模拟器上,用Monkey去点点点,只要app真的有隐私Api的调用,总归有概率能捕捉到,然后引入人工review,整体思路也比较直白。

智能化

围绕崩溃的一系列设想

下面很多方向可能已经与Monkey测试本身无关了,属于上下游的拓展,是整个稳定性解决方案的一环。实践中主要几个关键问题:

  • 崩溃重要性的识别 —— 崩溃已经发生了,能否有人告诉我它到底重不重要,需不需要马上修,还是晚点修,或者不修
  • 崩溃自动归因、聚类、推荐 —— 基于堆栈和其他现场信息,能否告诉我大致的排查方向,别人发生过的类似问题是怎么修复的
  • 崩溃自动修复 —— 虽然有点玄幻,但是历史积累了那么多崩溃和修复patch,一些简单的问题确实能够自动修复,比如NPE,多数场景确实只增加一个判空即可(也有不少场景除了判空还要做其他业务逻辑才是有效修复,这种一般handle不了)
  • 问题易发的路径 —— 根据历史存量已修复的崩溃,自动猜测什么场景什么操作最高概率出现崩溃,从而定向自动地去做测试
  • 监控规则智能补齐 —— 结合线上监控,自动分析逃逸问题的崩溃或实现模式,总结规则提前避免

自动断言

这是一个比较广的命题,它可以包括:功能问题分析Fastbot:自动功能测试探索 、GUI兼容分析、GUI异常分析等一系列子方向。

参考手工测试的行为模式,更多也是在图像、日志上下手做分析实现问题诊断。

商业化

成本中心转化为利润中心,ToB的建设比不可少。对外以云真机平台的形式展示,作为一个测试能力选项对外暴露,这里只要保证Monkey客户端被正确混淆,不泄露代码出去即可,其他均可依赖云真机平台去实现ToB计费策略与使用入口。ToB需要做的事情明显简单很多。


Monkey测试落地阶段

在实际落地过程中,一般要综合考虑几个要素,以此选择不同的测试策略和制定不同目标:

  • app所处的质量阶段
  • 所拥有的测试机器资源
  • 持续集成频率或负荷

业务早期,app处于堆功能阶段,为了追赶时间窗口,质量必然是次要考虑的事情。app质量分全局质量和局部质量,为了最大化测试资源利用,一般来说全局质量比局部质量更为重要,直接上Monkey全局跑。

业务中期,app功能迭代稳定,开始围绕着质量从不同方向上寻求突破,倡导测试左移,Monkey会更前置到各个Merge Request中执行测试,开始出现定向测试、拟人测试、安全测试等能力积累。

业务后期,app功能基本稳定,更多时间是做技术优化和质量提升,追求极致体验,挖掘更极端的问题。此时Monkey的定位慢慢弱化成一个UI遍历驱动器,在这个遍历驱动器上附着不同的测试工具,每种工具都能拦截一类专项问题,通过这样的手段逐渐往极致靠拢。


参考资料

外部资料:

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2017-2022 Zingphoy Han
  • 访问人数: | 浏览次数:

一块钱一个俯卧撑 O_O

微信