阅读 2011

Android调用链——自动化精准测试

作者:字节跳动终端技术——吴思成

通过Android调用链算法落地自动化测试场景,能够提供自动的精准的测试能力,从而提高代码的质量保障以及减少测试的人耗。

一、背景

自动化精准测试是指对每次MR中改动部分的代码,能够进行自动的准确的测试,从而提高代码的质量保障以及减少测试的人耗。

现有流程

常规的开发流程如下:

为了确保这些变动不会引入crash,影响线上用户体验,因此需要对这些变动进行测试。

测试一般分为开发同学白盒自测以及测试同学黑盒测试

1

目前测试的流程如下:

除去开发自测之外,还需要测试同学来进行测试,常规的测试手段就是针对应用的每个Activity维度去录制测试用例,在每次提交mr的时候,测试的同学跑一下测试用例即可。

对于每次提交mr,我们对代码所发生的变动抽象为如下三种情况:

  1. 添加了新的方法
  2. 改动原有方法
  3. 删除方法

对于第一种情况,可能是添加了新的逻辑,也可能是新增了功能,因此现有的测试用例可能无法覆盖到新功能,需要测试同学补充录制测试用例。

对于后面两种情况,无论是方法的逻辑修改还是方法删减,现有的测试用例能够覆盖代码改动逻辑,因此测试的同学只需执行现有的测试用例即可

自动化测试流程

手动执行已有的测试用例其实是一个重复机械的工作,因此我们把这个流程改造成了自动化流程:

如上图所示,我们把自动化测试添加到了CI流程当中,依赖于「构建包」任务获取apk包。并且还使用到了公司内部的云真机平台,即我们可以直接通过http请求接口让测试机执行我们的自动化测试脚本,从而执行测试用例。

云真机平台界面

实际流程跑通之后,很快我们遇到了一些新的问题:

  • 像抖音、头条的团队,每天都有成百上千个mr,如果每个mr都全量跑测试用例,那么每个测试会特别耗时且极其耗费云真机资源
  • 此外,其实大部分mr的代码改动量并不大,每次可能只涉及几个函数的变动,因此使用全量测试用例显然不合理

因此我们需要针对每次mr去寻找合适的测试用例,精准的推荐到自动化测试流程当中。

二、精准测试方案

问题描述

我们希望在每次mr的时候,能够推荐和本次mr变更代码相关的测试用例,从而进行自动化测试。

那么需要面临如下几个问题:

  1. 如何将测试用例和代码关联
  2. 如何获取每次mr的变更内容
  3. 如何精准推荐测试用例

1. 测试用例如何关联代码

测试用例实质是黑盒测试时的点击输入等一系列用户行为的录制,那么我们如何能够将这些用户行为和实际的代码对应上呢?

连结点其实就在Activity上,上文提到录制测试用例时,测试同学是以Activity作为维度进行录制的,那么如果我们能够知道当前的代码关联哪个Activity,就可以只用这个Activity的测试用例来测试这段代码,能够有效的减少测试案例的数量,提高测试的效率以及精确度。

80

那么问题来了,我们如何知道某段代码关联哪个Activity呢?

这里可以通过生成方法调用链来实现

什么是方法调用链

就是将一段代码中的所有函数的调用关系通过调用边连接形成图,这个图就是方法调用链图

Android调用链

如果能够找到Activity的直接关联的函数,并且结合方法调用链,我们就能够找到Activity所间接关联的函数。

如图,function1是ActivityA直接关联的函数,那么function1这条调用链上的其它函数都间接地与ActivityA关联。

我们称这种具备Activity到函数的边的图为Android调用链图,下文中我们会着重地介绍如何生成一个Android调用链图。

Android调用链应具备能力:

  • 从 Activity 查询所有该 Activity 涉及的函数(无层级关系)
  • 从 函数 查询所有涉及该函数的 Activity
  • 查询函数调用关系,一跳,二跳等
  • 查询某个 Activity 的起始函数
  • 查询某个 Acitivity 的下一个 Activity

2. 获取mr变更的内容

可能有人会想:获取变更内容,难道不是求一下mr前后commit的diff就完事了吗?

但其实并没有这么简单,因为我们求出来的diff只是增删改的代码段,而单凭代码段是没有办法通过Android调用链关联到Activity的。Android调用链的节点是方法,因此我们实际需要的mr变更内容应该是本次mr中发生变更的方法,这里指的方法变更包括:

  1. 方法新增
  2. 方法改动
  3. 方法删减

那么我们如何知道一次mr中有哪些方法发生变动呢?

这里我们使用到了静态分析的技术,首先获取本次mr中所有发生变更的源码文件,以及其对应的变更前的源码文件。然后通过intellij的sdk将源码文件转化为psi,最后通过对比psi能够获取变更的方法有哪些。

80

PSI:程序结构接口,是IntelliJ Platform中的一个语义抽象层,负责解析文件并创建支持平台许多功能的语法和语义代码模型。我们可以简单的把它理解为是一个抽象语法树,但是它基于java以及kotlin的语言特性做了更细粒度的解析,能够识别出代码中的类、方法、参数、判断符等语义。

因此基于psi,我们比较两个文件中方法是否发生了变更就会简单很多,比较规则如下:

  1. 新增方法,比较新文件和旧文件中的方法名,如果某方法只在新文件中存在,而旧文件中不存在,则表示该方法为新增方法
  2. 删除方法,比较新文件和旧文件中的方法名,如果某方法只在旧文件中存在,而新文件中不存在,则表示该方法为删除方法
  3. 改动方法,如果新旧文件中都存在该方法,那么分别计算出新旧文件中该方法的body的size,如果size不一致,则表示方法发生了变动

3. 精准推荐测试用例

对于测试用例的推荐,并不仅仅只是过滤出相关的Activity用例,还会结合Activity与这次变更的相关性、Activity是否是线上热点Activity、Activity的发现关联Crash的后验概率等信息,去设置Activity的测试步数。此外,还会基于测试覆盖率、crash率、线上用户机型分布等多维度数据对目标Activiy的测试机型进行分配。具体的推荐算法流程目前暂不便于对外,敬请期待后续的分享。

三、Android调用链构建流程

阶段一:生成全局函数调用链图

简单介绍一下知识背景,调用链是基于静态分析技术实现的,静态分析技术可简单分为源码分析和产物分析,例如Android所提供的Lint检测就是基于源码分析,而这里生成调用链是基于apk分析,也就是产物分析。

目前针对Java开源的静态分析框架,主要有walaSoot,相比wala,Soot的文档更多,社区更为活跃,因此我们最终基于Soot进行定制开发

我们所开发的Android精准调用链生成工具——ByteRope,是基于Soot定制化开发,Soot为我们提供了CallGraph的生成能力,但是简单的CallGraph并不能满足我们对于精准关联Activity的需求,还需进一步的优化改造。

简单介绍一下调用链生成的算法流程:

调用链生成流程

  1. 解析apk,获得apk中所有的class
  2. 解析每个class,获得class中的所有method
  3. 解析所有method,获得method的body,body是由一条条命令语句组成,例如复制、方法调用等
  4. 解析method的body,一旦出现函数调用,就在这个method和被调用的method之间构建一条边
  5. 当我们遍历完所有class中的所有method's body,那么调用链图也就构建完成了

在构建调用链图过程中我们能够拿到的信息:

  1. apk中全部的类
  2. 每个类中的方法
  3. 每个方法的body
  4. 每个方法所调用的其他方法(调用边)

阶段二:构建Activity-method调用链路

一、获取apk中的所有Activity

前面提到,调用链的目的是找到方法所关联的Activity,从而推荐自动化测试case,因此我们需要找到所有的Activity,将其作为调用链的入口类。

获取Activity方法

前面提到,在生成调用链的过程中,我们已经拿到了apk中所有类的信息,只需要遍历所有类,判断该类是否继承于android.app.Activity、androidx.appcompat.app.AppCompatActivity,如果继承,则表示该类为Activity。

60

二、生成以Activity为入口的调用链

在阶段一中已经生成了全局调用链,这里以Activity为入口生成调用链的目的是确认调用链中的函数都是由Activity出发链接的,从而确保调用链中的每个方法都关联了Activity

"以Activity作为入口生成的调用链"

以Activity作为入口生成的调用链

四、调用链的优化:关联Android原生组件

前面提到,Soot为我们提供了CallGraph的生成能力,但是简单的CallGraph并不能满足精准关联Activity的需求,还需进一步的优化改造。

背景

Android中很多组件、控件是通过布局文件或是异步机制调用的,因此即使生成了全局调用链,也难以将这些组件、控件和所属的Activity关联起来。

调研

可能会出现这种情况的组件有Fragment,自定义控件。

Fragment

其中Fragment一般分为静态加载动态加载.

  • 静态加载是在activity的布局文件中进行载入
  • 动态加载一般是通过FragmentTransaction.add(fragment).commit()载入。

自定义控件

一般继承自View或ViewGroup,加载方式也分为静态加载和动态加载

  • 静态加载是在layout文件中直接使用控件的全限定名作为Tag
  • 动态加载

一般是通过**ViewGroup.addView()**将自定义控件装载至目标ViewGroup中

方案

关于显式调用Fragment、自定义View

面临的问题

调用链不会关联系统函数,因此Fragment、自定义View下的Android系统override方法是不会被关联到的。

80

建模

已有的调用链Fragment的系统override方法关联起来。

但是由于系统函数本来就没办法直接和其他方法进行关联,因此我们手动添加一条边,将caller方法和override方法关联起来。

70

caller方法和override方法关联

关于静态调用布局文件中的Android组件、Fragment

面临的问题

由于通过布局文件加载的Android组件完全是走Android系统内部的逻辑,并且是异步调用的方式,因此当前生成的调用链不存在由Activity到这些Android组件的通路,换句话说,这些组件无法找到它们所关联的Activity,从而导致精准测试无法推荐测试用例。

建模

目的:构建从 Activity 到 被静态调用的组件 的通路。

思路:寻找静态调用的衔接点,需要找到布局文件调用Android组件整个流程的所有衔接点,才能够串联成调用链通路:

  1. Activity通过setContentView()设置布局文件

能够拿到Activity对应的布局文件id

  1. 找到布局文件id与布局文件的映射关系

首先,布局文件是存放在apk中的,我们解压apk,就能够发现布局文件存放在res目录下。

其次,在apk中有一个二进制文件名字叫resources.arcs,这是apk中所有资源信息的集合,我们所需要的 布局文件id 到 布局文件 的映射关系就存放在resources.arcs文件中。

  1. 解析布局文件

首先,需要了解Activiy是如何在布局文件中使用Fragment以及自定义组件的:

​ a. 布局文件中调用Fragment

使用 标签,并在android:name属性中引入Fragment类名:

​ b. 布局文件中调用自定义View

直接调用自定义控件类作为layout.xml的tag

​ c. 布局文件中调用其他布局文件

​ i. 通过标签引入其他布局

​ ii. 通过android:layout属性引入其他布局

其次,解析布局文件,根据上述的Activity通过布局文件静态配置组件的方式,设置文件解析规则,从而获取Activity到Fragment、自定义View的链路:

67

至此,Android调用链关联Android原生组件的优化工作已完成。

五、收益

在5-6月中,自动化精准测试接入至抖音的MR流程,目前已取得了初步的成效:

  1. 抖音Android: 工具线+基础业务+社交 的测试人效节省35%
  2. 相比普通自动化任务(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,邮件主题 简历-姓名-求职意向-期望城市-电话

文章分类
Android
文章标签