本文整理自美团技术沙龙第 77 期《美团亿级流量系统的质量风险防控和稳定性治理实践》,主要介绍了对网络返回数据进行变异的客户端健壮性测试实践经验。文章第一部分介绍客户端健壮性测试的基本概念;第二部分分享了基于接口返回数据变异的 App 健壮性测试方案设计的思路;第三部分主要解读了变异数据的构造和异常检测方案设计;第四部分介绍了精简变异数据的探索方案。
什么是客户端健壮性
在维基百科的定义中,健壮性(Robustness)是指一个计算机系统在执行过程中处理错误,以及算法在遭遇输入、运算等异常时继续正常运行的能力。IEEE 中将健壮性定义为系统或组件在存在无效输入或压力环境条件下可以正常运行的程度。早在 1989 年,Barton Miller 首次提出了模糊测试的概念,通过向目标应用抛出随机字符串的方式来测试 UNIX 应用程序的健壮性;而在 1996 年的 Ballista 项目中,研究人员探索根据 API 定义的数据类型,对操作系统或软件接口进行自动化测试方法。两个项目均以“无应用程序崩溃或挂起”作为测试验证通过的标准。
在移动端 App 领域,健壮性可以理解为 App 运行时遭遇环境异常或者输入异常时客户端能够继续正常运行的能力。
其中,环境异常主要分为操作系统异常、外部环境异常、硬件环境异常三大类。比如内存不足、CPU 负载过高、线程池满载、内存分配失败、网络连接失败等。输入异常主要分为系统输入和用户输入。比如网络接口返回的数据异常、应用内缓存、数据库文件读写异常,这类的异常属于在系统输入异常;在电话号码输入框场景,用户输入的空格、富文本则属于用户输入异常。
对于这些风险,如果 App 没有处理,理论上都可能会产生展示异常、交互异常、性能、安全等问题,导致用户无法继续使用或在使用过程中产生不好的体验。比如用户操作 App 下单过程中,API 请求出现故障未返回状态码为 200 的响应,App 由于没有获取到预期接口响应的信息而发生崩溃,就会中断用户的使用流程。
基于接口数据变异的 App 健壮性测试方案设计
在实际的客户端测试执行过程中,测试人员会考虑测试异常输入的场景,但由于成本无法做到无穷尽的测试,同时还存在人工执行遗漏的风险。
从美团 App 平台业务的历史故障分析中,我们发现:网络请求返回的数据与实现预期不符引发的 Crash 或核心功能缺失问题导致的故障占比最高,且影响面较广。比如接口返回非预期数据时,客户端处理数据类型转换异常导致闪退,即使 5 分钟内操作降级仍影响了百万量级的用户。因此美团平台业务 App 的健壮性测试探索优先从发现网络请求返回数据导致的异常开始。
针对于发现请求接口返回客户端非预期数据导致的 Crash,或者核心模块缺失问题这个诉求,我们调研后发现方案的基本原理都是相似的,即以网络请求的原始响应为基础,根据规则进行变异构造,使用代理工具改写响应体返回给客户端,在端上设备做异常检测。但是都存在一些问题不能满足诉求,比如测试变异数据是根据预置或者自定义规则随机生成组合,随机性过大,不能有效拦截健壮性问题;但如果不做随机,产生的用例组合量过大,测试不能在合理时间范围内结束;另外在检测能力方面,不具备发现业务异常或功能模块异常的能力。
因此,我们结合通用方案做了一些自定义改造,整体检测方案包含静态检测和动态检测两部分。
静态检测,主要是指静态代码扫描,将典型代码编写规范问题转化为自定义静态代码扫描规则,管控增量代码,同时长期治理存量风险。比如自定义了 PrimitiveParseDetector、ColorParseDetector,管控业务必须使用健壮性测试通过的工具类。
动态检测,是指结合触发时机,构造并注入变异数据后,识别 App 运行时是否出现崩溃、挂起或业务功能模块异常。比如在集成事件/回归事件触发自动化测试运行,构造触发异常的数据进行动态测试,然后监测是否出现了异常。核心动作包含构造变异数据和完成检测两部分。比如将接口响应体中表示颜色含义的 Key 对应的 Value 值构造成非色值,然后检测客户端请求处理接口数据时是否出现崩溃或挂起。
下文重点介绍端到端的动态检测方案。
变异数据的构造和异常检测
对于美团 App 来说,首页有多种形态,对于某种特定形态,除了控制请求数据外还需要控制实验、策略等一系列因素,才能保证测试对象的唯一性。一个页面中包含多个异步请求,因此请求的构造也需要和页面路径关联。这些都是采集变异所需的基础数据时需要关注和控制的。
响应体由基本类型数据和复合类型数据组成,相同基本类型的数据可能具备不同的业务语义,需要根据语义的类型做变异规则的区分对待,才能保障业务场景覆盖。
因此,如何保障变异数据构造的全面性和准确性,是我们面临的首要挑战。
要解决数据构造全面性问题,首先要解决页面描述方案,这样才能控制获取基础数据的唯一性。在解决方案中,我们构建了页面描述的特征规则,解决用户视角的页面标识问题。需要的信息包含端信息、页面路由信息、实验策略账号信息、页面标识模块合集等。通过页面请求数据自动录制的方式,自动更新迭代请求数据和页面之间的绑定关系,使得基础数据能够随需求迭代更新,从而通过变异规则构造生成的用例也能够自动更新。
在用例变异生成构造上,对于响应体里的 Value 设置了语义匹配规则,比如字符串的语义可能代表颜色、页面跳转路由、动静态资源链接(即图片资源数据/视频文件/GIF 文件),需要区分特征分别按语义构造异常数据。
比如在图片的变异数据构造里,除了需要构造非图片链接情况外,还要考虑不同图片格式、非图片格式以及非合法的图片剪裁格式拼接等场景。
我们对接口返回数据使用脚本做了初步的语义分析,人工二次校正后建立了基本数据类型和语义的映射集合,结合基本数据类型边界值和语义定义了初始的变异规则。然后对历史的线上健壮性问题和线下测试发现的健壮性 Bug 的变异数据进行整理,作为增补的变异规则。
在自动化测试执行过程中,我们基于 App 可测性改造提供的能力,对测试场景进行了控制,同时基于布局视图的解析 SDK、App 异常上报 SDK 提供的能力,完成了对 App 异常的通用检测。
变异数据的精简方案
伴随着变异规则的丰富,自动生成的数据量级是巨大的,数据的变异组合如果按照全覆盖方式来生成组合数量就是指数级增长。比如对于 1 种有 7 种变异取值的变量,如果存在 n 个此类型变量,就会产生 7^n 种数据组合,并且在实际业务场景中很多组合情况是没有意义的。
如何在保障用例构造全面性的情况下精简变异构造的用例数,是我们面临的第二个挑战。解决方案包含 2 个策略:1)数组元素结构一致时,删减构造的用例数;2)结构不完全一致的数组元素,引入编辑距离和并查集算法判断节点相似性,节点不相似,可以在一次数据生成里做合并构造。
我们可以把请求响应的 JSON 理解成树,第一个解决思路是判断树中节点、路径的相似度,相似节点删减构造。
如果路径、节点相似,可以推测路径即业务逻辑也是一致的,比如页面上的一些列表元素,可能是数据结构对象完全一致数组,如果对每个数组对象中的每个元素进行全用例构造,生成的变异数据量极大,且对业务场景或代码逻辑的增量覆盖有限,因此我们决定将构造逻辑优化,进行删减构造。即假如数组中元素的结构完全一致,那么同含义的字段可以为他们分配不同的变异构造值,然后删减掉无效的构造情况。应用这种方法可以有效降低 28%左右的用例构造数量。
如图数组的 3 个元素中均存在“resourceName”键值对,假如每个键值对有 3 种变异取值,按照全排列方式进行用例构造将会生成有 9 份变异数据,在删减构造情况下,可以分别为它们构造一个特定的变异值,这样变异生成用例数量可以从 9 减少为 1。
在对业务接口返回数据的数据结构进行分析后,我们发现在层级越深的场景下,距离根节点越近的两个节点,业务逻辑耦合和结构相似程度越低,它可以进行合并构造,相互逻辑之间不会产生影响,比如有两个键值对,每个键值对的 Value 有 3 种变异取值,在合并构造情况下,可以从排列组合的 6 份数据减少到 3 份数据。
基于这个思路,我们在实践中引入了编辑距离和并查集算法,以节点路径为参照,对树的每一层的每两个节点计算编辑距离,生成一个 n*n 矩阵;同时以树的高度减去节点位于的层数作为权重,修正编辑距离。基于这样的计算,会产生多个编辑距离矩阵。
为了尝试最大化合并构造用例效果,我们把编辑距离做了 0,1 矩阵转化。其中,由于编辑距离为 1 的两个节点可能存在业务逻辑耦合关系,必须放在同一个组里分别构造,所以我们把编辑距离大于 1 的情况转化成了 0,最后得到了一个 0,1 的编辑距离矩阵。
在 0,1 矩阵情况下,我们使用了图的连通性概念,如果 A 和 B 连通,B 和 C 连通,那我们认为 A 和 C 连通,转化到这里的概念就是 A 和 B 相似,B 和 C 相似,那么 A 和 C 相似,它们应该被放在同一个组里分开进行构造,那么在同层元素构造时,我们会从每个分组里取到一个节点,对这些规则进行变异组合构造。
基于以上两个策略进行精简后生成的变异数据量较精简前降低了 40%,同时代码覆盖率没有明显变化,并且保持不变的健壮性问题发现能力。
美团 App 和优选 App 都接入了这个工具,在新需求阶段可以人工触发运行,还可以结合客户端组件集成事件和回归事件做自动触发。至今应用一年时间内,发现了几十个问题。
总结及展望
在健壮性工具建设一期里,我们实现了 App 页面加载展示场景的健壮性问题检测,支持崩溃、卡死和部分功能异常这三类异常检测。另外,基于节点相似性优化变异数据生成策略能够在保持效果不变的情况下有效控制测试时长,但是否有更优的合并算法和推荐算法,还需要更多的尝试。
在后续工具的迭代还会继续围绕异常构造和异常检测这两个方向,支持更丰富的构造能力和检测能力,以及更高效的构造效率。
短期建设上,我们将会从业务视角出发丰富自动化变异数据生成建模,完善客户端异常通用异常检测能力,完成通用前后端交互的数据构造类型(比如:长连接消息)的覆盖;长期建设上,需要支持更丰富的数据和环境构造能力,通过智能化用例生成,提升测试效率。
Q&A
Q1:节点相似的判断依据是什么?
A:从实际的 response 分析来说,两个节点的路径完全相似就是从根节点到最终的叶子节点上,它们的路径命名完全相似,数组里两个对象的结构完全一样。
Q2:用例的生成能举个例子吗?
A:比如颜色色值的格式是 #+6 位字符,通常运营配置会出现的情况是忘记添加 #,或色值复制中少了一位。在这种情况下,我们会构造一个色值,比如没有返回 #、色值位数不对、色值添加透明度,把这种场景作为构造情况,在配置里添加上,最后用代码生成。
Q3:健壮性平时执行的频率是什么样的?
A:第一个基于需求维度,需求维度需要人工触发;第二个基于变更维度,当组件发生变更时,可以关联到这段代码或者组件变更的页面,然后触发页面对应的健壮性测试,执行频率会受到组件变更频率的影响;第三个在回归测试时,App 的回归测试两周一次,我们会把所有页面以及它关联的所有的用例都执行一次。
Q4:对于暴露给前端开发的接口,大部分是人为调用参数的变化,随机性相对比较高,对于必填和非必填参数如何确认用例的范围?
A:目前我们在实现的方案里,没有区分参数是必填参数还是非必填参数,所以对于整个数据接口返回里的所有结果都会进行构造,产生的问题是对于非必返回的参数可能产生的问题,到底是否是需要解决的问题,这部分目前通过运营手段做确认。
Q5:首页可能调用 10 个接口,然后针对每个字段都进行异常验证吗?
A:对于首页关联的接口,我们在接口请求、录制过程中和录制完数据后,会对接口进行确认到底有哪些接口是我们需要验证的,这是一次性的成本,录制完成后,会对每个字段都进行异常验证,当然会有一些黑白名单的设置。
Q6:对色号这种情况有一种生成规则嘛,这个规则是怎么制定?
A:刚刚我只是举了一个色号的例子,其实对于图片、请求的资源文件、配置文件、跳转链接,每一个对应到的业务语义,我们都有对应的用例生成规则,我们会根据参考依据,比如第一个是本身我们在通用的基础库里怎么处理这些问题,这里有一个基础的规则;第二个是我们积累了线上问题情况实际可能会产生的错误或者变异情况,生成第一版基础规则,在第一期工具里找相关研发达成共识,这样的话,数据变异是处于合理范围。
Q7:执行的时候,如何知道页面对应哪些规则提前配置?
A:执行时,在测试接入过程中有一个配置过程,它不是配置这个页面和接口的关联关系,而是配置我们要测试哪些页面,自动触发自动化录制过程,就是到这个页面时,会触发哪些接口请求,生成这个页面和这个接口请求的对应关系,给到对应的配置人做确认,保证哪些接口是真正可能想要构造的,哪些接口不需要构造,最后以这个为基准测试,基于录制过程,比如业务迭代里面产生了新接口,我们在录制中能够感知到它关联的接口发生了变化,在发生变化时发消息给对应的测试提交人/负责人,TA 确认这条规则放到黑名单里还是更新到需要构造的接口里。
Q8:是否有做页面显示的一个校验?怎么做的?
A:目前我们在页面里的模块做了“是否展示”校验,基于当前集成到美团的可测性 SDK,这个 SDK 会获取到当前页面是否渲染里是否展示了对应模块的信息,通过请求把对应模块描述传给 SDK,通过返回来校验是否展示。
参考资料
[1] 健壮性
[2] IEEE健壮性
[3] Ballista:Carnegie Mellon 大学的研究项目,通过黑盒自动化测试的方式,发现导致系统崩溃或异常终止的系统调用或接口调用。
[4] 基于布局视图的解析 SDK:美团App页面视图可测性改造实践-XraySDK
评论