作者:字节跳动终端技术——吴思成
一、背景自动化精准测试是指对每次 MR 中改动部分的代码,能够进行自动的、准确的测试,从而提高代码的质量保障以及减少测试的人耗。
1.1 现有流程
常规的开发流程如下:
为了确保这些变动不会引入 crash,影响线上用户体验,因此需要对这些变动进行测试。
测试一般分为开发同学白盒自测以及测试同学黑盒测试。
目前测试的流程如下:
除去开发自测之外,还需要测试同学来进行测试,常规的测试手段就是针对应用的每个 Activity 维度去录制测试用例,在每次提交mr的时候,测试的同学跑一下测试用例即可。
对于每次提交 mr,我们对代码所发生的变动抽象为如下三种情况:
对于第一种情况,可能是添加了新的逻辑,也可能是新增了功能,因此现有的测试用例可能无法覆盖到新功能,需要测试同学补充录制测试用例。
对于后面两种情况,无论是方法的逻辑修改还是方法删减,现有的测试用例能够覆盖代码改动逻辑,因此测试的同学只需执行现有的测试用例即可。
1.2 自动化测试流程
手动执行已有的测试用例其实是一个重复机械的工作,因此我们把这个流程改造成了自动化流程:
如上图所示,我们把自动化测试添加到了 CI 流程当中,依赖于「构建包」任务获取 apk 包。并且还使用到了公司内部的云真机平台,即我们可以直接通过 http 请求接口让测试机执行我们的自动化测试脚本,从而执行测试用例。
云真机平台界面
实际流程跑通之后,很快我们遇到了一些新的问题:
- 像抖音、头条的团队,每天都有成百上千个 mr,如果每个 mr 都全量跑测试用例,那么每个测试会特别耗时且极其耗费云真机资源
- 此外,其实大部分 mr 的代码改动量并不大,每次可能只涉及几个函数的变动,因此使用全量测试用例显然不合理
因此我们需要针对每次 mr 去寻找合适的测试用例,精准的推荐到自动化测试流程当中。
二、精准测试方案
问题描述
我们希望在每次 mr 的时候,能够推荐和本次 m r变更代码相关的测试用例,从而进行自动化测试。
那么需要面临如下几个问题:
- 如何将测试用例和代码关联
- 如何获取每次 mr 的变更内容
- 如何精准推荐测试用例
2.1 测试用例如何关联代码
测试用例实质是黑盒测试时的点击输入等一系列用户行为的录制,那么我们如何能够将这些用户行为和实际的代码对应上呢?
连结点其实就在 Activity 上,上文提到录制测试用例时,测试同学是以 Activity 作为维度进行录制的,那么如果我们能够知道当前的代码关联哪个 Activity,就可以只用这个 Activity 的测试用例来测试这段代码,能够有效的减少测试案例的数量,提高测试的效率以及精确度。
那么问题来了,我们如何知道某段代码关联哪个 Activity 呢?
这里可以通过生成方法调用链来实现。
2.1.1 什么是方法调用链
就是将一段代码中的所有函数的调用关系通过调用边连接形成图,这个图就是方法调用链图:
2.1.2 Android 调用链
如果能够找到 Activity 的直接关联的函数,并且结合方法调用链,我们就能够找到 Activity 所间接关联的函数。
如图,function1 是 ActivityA 直接关联的函数,那么 function1 这条调用链上的其它函数都间接地与 ActivityA 关联。
我们称这种具备 Activity 到函数的边的图为 Android 调用链图,下文中我们会着重地介绍如何生成一个 Android 调用链图。
Android 调用链应具备能力:
- 从 Activity 查询所有该 Activity 涉及的函数(无层级关系)
- 从 函数 查询所有涉及该函数的 Activity
- 查询函数调用关系,一跳,二跳等
- 查询某个 Activity 的起始函数
- 查询某个 Acitivity 的下一个 Activity
2.2 获取 mr 变更的内容
可能有人会想:获取变更内容,难道不是求一下 mr 前后 commit 的 diff 就完事了吗?
但其实并没有这么简单,因为我们求出来的 diff 只是增删改的代码段,而单凭代码段是没有办法通过 Android 调用链关联到 Activity 的。Android 调用链的节点是方法,因此我们实际需要的 mr 变更内容应该是本次mr中发生变更的方法,这里指的方法变更包括:
那么我们如何知道一次 mr 中有哪些方法发生变动呢?
这里我们使用到了静态分析的技术,首先获取本次 mr 中所有发生变更的源码文件,以及其对应的变更前的源码文件。然后通过 intellij 的 sdk 将源码文件转化为 psi,最后通过对比 psi 能够获取变更的方法有哪些。
PSI:程序结构接口,是IntelliJ Platform 中的一个语义抽象层,负责解析文件并创建支持平台许多功能的语法和语义代码模型。我们可以简单的把它理解为是一个抽象语法树,但是它基于java以及kotlin的语言特性做了更细粒度的解析,能够识别出代码中的类、方法、参数、判断符等语义。
因此基于psi,我们比较两个文件中方法是否发生了变更就会简单很多,比较规则如下:
- 新增方法,比较新文件和旧文件中的方法名,如果某方法只在新文件中存在,而旧文件中不存在,则表示该方法为新增方法
- 删除方法,比较新文件和旧文件中的方法名,如果某方法只在旧文件中存在,而新文件中不存在,则表示该方法为删除方法
- 改动方法,如果新旧文件中都存在该方法,那么分别计算出新旧文件中该方法的 body 的 size,如果 size 不一致,则表示方法发生了变动
2.3 精准推荐测试用例
对于测试用例的推荐,并不仅仅只是过滤出相关的 Activity 用例,还会结合 Activity 与这次变更的相关性、Activity 是否是线上热点 Activity、Activity 的发现关联 Crash 的后验概率等信息,去设置 Activity 的测试步数。此外,还会基于测试覆盖率、crash 率、线上用户机型分布等多维度数据对目标 Activiy 的测试机型进行分配。具体的推荐算法流程目前暂不便于对外,敬请期待后续的分享。
三、Android 调用链构建流程
3.1 阶段一:生成全局函数调用链图
简单介绍一下知识背景,调用链是基于静态分析技术实现的,静态分析技术可简单分为源码分析和产物分析,例如 Android 所提供的 Lint 检测就是基于源码分析,而这里生成调用链是基于 apk 分析,也就是产物分析。
目前针对 Java 开源的静态分析框架,主要有 wala 和 Soot,相比 wala,Soot 的文档更多,社区更为活跃,因此我们最终基于 Soot 进行定制开发。
我们所开发的 Android 精准调用链生成工具——ByteRope,是基于 Soot 定制化开发,Soot 为我们提供了 CallGraph 的生成能力,但是简单的 CallGraph 并不能满足我们对于精准关联 Activity 的需求,还需进一步的优化改造。
简单介绍一下调用链生成的算法流程:
调用链生成流程:
- 解析 apk,获得apk 中所有的 class
- 解析每个 class,获得 class 中的所有 method
- 解析所有 method,获得 method 的 body,body 是由一条条命令语句组成,例如复制、方法调用等
- 解析 method 的 body,一旦出现函数调用,就在这个 method 和被调用的 method 之间构建一条边
- 当我们遍历完所有 class 中的所有method's body,那么调用链图也就构建完成了
在构建调用链图过程中我们能够拿到的信息:
- apk 中全部的类
- 每个类中的方法
- 每个方法的 body
- 每个方法所调用的其他方法(调用边)
3.2 阶段二:构建 Activity-method 调用链路
3.1.1 获取 apk 中的所有 Activity
前面提到,调用链的目的是找到方法所关联的 Activity,从而推荐自动化测试 case,因此我们需要找到所有的 Activity,将其作为调用链的入口类。
获取 Activity 方法:
前面提到,在生成调用链的过程中,我们已经拿到了 apk 中所有类的信息,只需要遍历所有类,判断该类是否继承于 android.app.Activity、androidx.appcompat.app.AppCompatActivity,如果继承,则表示该类为 Activity。
3.2.2 生成以 Activity 为入口的调用链
在阶段一中已经生成了全局调用链,这里以 Activity 为入口生成调用链的目的是确认调用链中的函数都是由 Activity 出发链接的,从而确保调用链中的每个方法都关联了 Activity。
以Activity作为入口生成的调用链
四、调用链的优化:关联 Android 原生组件
前面提到,Soot 为我们提供了 CallGraph 的生成能力,但是简单的 CallGraph 并不能满足精准关联 Activity 的需求,还需进一步的优化改造。
4.1 背景
Android 中很多组件、控件是通过布局文件或是异步机制调用的,因此即使生成了全局调用链,也难以将这些组件、控件和所属的 Activity 关联起来。
4.1.1 调研
可能会出现这种情况的组件有 Fragment,自定义控件。
Fragment
其中 Fragment 一般分为静态加载和动态加载.
- 静态加载是在 activity 的布局文件中进行载入
- 动态加载一般是通过 FragmentTransaction.add(fragment).commit() 载入。
自定义控件
一般继承自 View 或 ViewGroup,加载方式也分为静态加载和动态加载
- 静态加载是在 layout 文件中直接使用控件的全限定名作为 Tag
- 动态加载
一般是通过 ViewGroup.addView() 将自定义控件装载至目 标ViewGroup 中。
4.2 方案
4.2.1 关于显式调用 Fragment、自定义View
面临的问题:
调用链不会关联系统函数,因此 Fragment、自定义 View 下的 Android 系统 override 方法是不会被关联到的。
建模:
将已有的调用链和 Fragment 的系统 override 方法关联起来。
但是由于系统函数本来就没办法直接和其他方法进行关联,因此我们手动添加一条边,将 caller 方法和 override 方法关联起来。
caller 方法和 override 方法关联
4.2.2 关于静态调用布局文件中的 Android 组件、Fragment
面临的问题:
由于通过布局文件加载的 Android 组件完全是走Android 系统内部的逻辑,并且是异步调用的方式,因此当前生成的调用链不存在由 Activity 到这些Android 组件的通路,换句话说,这些组件无法找到它们所关联的 Activity,从而导致精准测试无法推荐测试用例。
建模
目的:构建从 Activity 到 被静态调用的组件 的通路。思路:寻找静态调用的衔接点,需要找到布局文件调用Android组件整个流程的所有衔接点,才能够串联成调用链通路:
- Activity 通过 setContentView() 设置布局文件:
能够拿到 Activity 对应的布局文件 id
其次,解析布局文件,根据上述的 Activity 通过布局文件静态配置组件的方式,设置文件解析规则,从而获取 Activity 到 Fragment、自定义 View 的链路:
至此,Android 调用链关联 Android 原生组件的优化工作已完成。
五、收益
在 5-6 月中,自动化精准测试接入至抖音的 MR 流程,目前已取得了初步的成效:
- 抖音 Android: 工具线+基础业务+社交 的测试人效节省35%
- 相比普通自动化任务(Activity 覆盖率约 0.5%),自动化精准的 Activity 覆盖率平均提升约15倍,任务平均可以发现约3个 Crash
六、总结与展望
本文首先介绍了自动化精准测试的演变过程,以及我们在实现自动化精准测试过程中遇到了哪些问题,及其解决的方案;其次,本文着重介绍了自动化精准测试流程中,Android 调用链作用、性质以及它的构建方式,并介绍了 Android 调用链的优化项,即基于 Android 特性定制化关联 Activity,使得mr变更方法关联 Activity 的准确度提升,从而提高测试用例的推荐准确率,减少不必要的测试,提高测试人效。
但是 Android 调用链的使用场景远不止于此,它还能够应用于敏感方法的链路追踪、API 调用梳理等场景,但随之而来的是对调用链精确程度的要求提升,因此我们对 Android 调用链的优化做出如下的几点展望:
- 完善调用链算法,目前调用链的构建仅局限于同步调用,但在 Android 中存在很多异步调用的逻辑,那么针对这些异步调用的场景,我们能够通过建模将其覆盖,从而提升调用链的精确度。
- 生成更细粒度的调用链,目前调用链是以方法粒度进行构建的,但是在方法中存在判断条件导致逻辑分叉,那么如果能够将方法基于代码语义拆分成 BasicBlock 粒度,并进行调用链的构建,就能够使得每条调用链所表示的逻辑信息更加精准,从而也能够在智能测试领域提升推荐测试 case 的准确度。
关于字节跳动终端技术团队
字节跳动终端技术团队(Client Infrastructure)是大前端基础技术的全球化研发团队(分别在北京、上海、杭州、深圳、广州、新加坡和美国山景城设有研发团队),负责整个字节跳动的大前端基础设施建设,提升公司全产品线的性能、稳定性和工程效率;支持的产品包括但不限于抖音、今日头条、西瓜视频、飞书、懂车帝等,在移动端、Web、Desktop等各终端都有深入研究。
就是现在!客户端/前端/服务端/端智能算法/测试开发 面向全球范围招聘!一起来用技术改变世界,感兴趣请联系chenxuwei.cxw@bytedance.com,邮件主题 简历-姓名-求职意向-期望城市-电话。
本文提到的自动化精准测试后续将在火山引擎应用开发套件MARS上线,MARS是字节跳动终端技术团队过去九年在抖音、今日头条、西瓜视频、飞书、懂车帝等 App 的研发实践成果,面向移动研发、前端开发、QA、 运维、产品经理、项目经理以及运营角色,提供一站式整体研发解决方案,助力企业研发模式升级,降低企业研发综合成本。可点击链接进入官网了解更多产品信息。
|