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