阅读 558

字节跳动服务端单测ATG-SmartUnit 探索实践

本文是字节跳动Quality Lab团队的刘冠成讲师在MTSC 中国测试开发大会中「字节跳动服务端单测ATG-SmartUnit 探索实践」的分享全文,评论区回复“MTSC”获取分享完整 PPT及技术交流群二维码。

单元测试是研发质量保障的重要环节。但单元测试编写成本高,开发人员积极性低,导致单元测试很难发挥最大的功效。对于如何⾼效构建单测,业内提供了不少精彩的解决⽅案,却少有全方位地解决单元测试代码智能⽣成领域的问题。为此字节跳动 Quality Lab攻克了很多难关,例如:

[1] 如何理解代码⼯程的语义;

[2] 如何进⾏代码⾃动⽣成;

[3] 如何生成测试用例可以覆盖更多代码分⽀;

[4] 如何保证代码元数据被最⼤化运⽤

最终完成了智能化单测产品【SmartUnit】,实现了单元测试用例的全⾃动⽣产和回归测试,并能够保证35%覆盖率和精准断言,本⽂我们将会针对我们单测ATG⽅⾯的实践经验进⾏分享。

项目背景

据统计,大部分的错误都是在项目初始阶段引入的,修正错误的费用随着项目的迭代逐步上升。单元测试的显著特点是约束了测试的状态规模,从而更好更快地发现问题。基于模块化拆解,它可以很好的管理项目变更,可以防止 bug 过多导致一个项目失控。它是一种非常高性价比质量保证手段。

单元测试的好处很多,但编写成本非常大。为了赶项目省时间,初期开发往往忽视写单元测试,将单元测试构建的时间推移到了项目的中后期,而这时 bug 都被用户体验过了,坑都被测试人员踩过了。这样导致了【项目开发周期】和【单元测试质量保证周期】的错位,严重浪费了单元测试本该起到的质量保证效果。

项目目标

单元测试难点

通过上文的分析,单元测试的困局是【作用大但成本高】,而编写成本主要归结在这个几个方面:

维度难点
业务知识有准入门槛:需要开发人员去了解相关的业务链条,和目标函数作用。这样才能编写出有意义的单元测试验证业务逻辑,否则单元测试可能无法有效检测业务异常。
人力成本非常耗费人力:需要停下业务开发,对业务函数构建大量代码来验证函数功能。
质量标准很难量化产出:单元测试编写的付出相较于业务开发体现不明显;难以衡量产出;不容易被认可。

SmartUnit切入点

为了解决这些问题,SmartUnit的切入点可以简单概括为:智能化、一体化、标准化。SmartUnit通过智能化的方式去解决人力成本的问题,使单元测试的代码编写自动化;我们把 SmartUnit项目嵌入到验收的环节(如CI流水线环节)当中,使得自动编写的单元测试在验收环境自动执行,可以无侵入地对函数做回归验证让单元测试在项目的初期发挥功效

除此之外,SmartUnit还对单元测试进行了标准化,定义了行覆盖率,分支覆盖率,分支距离等评判标准。

总结来说,智能化解放人力成本;一体化使得单元测试快速生效;标准化解决度量问题;比较完美地解决了单元测试实际运用中的主要矛盾。

项目目标

目前,SmartUnit自动生成的单元测试代码的平均覆盖率达到35%左右,结合静态代码分析和运行捕获可以会为项目检测出1~2个 bug,捕获8 个 panic。

SmartUnit目标是自动生成覆盖率达到40%~60%的单元测试,建设全自动化代码质量保障系统,并且基于自动生成的单元测试做更多的探索和实践,如bug自动修复与验证,数据流追踪分析等。

设计方案

用例生成简析

通过项目背景和切入点介绍,SmartUnit设计理念非常清晰:即智能化闭环。整体上看,SmartUnit的工作流程是:分析代码仓库、构造出测试用例、最终通过运行分析的方式筛选出最好的测试用例,以下的具体设计都是以 golang 语言作为例子

如上例举了几个技术点,如 SSA/AST 是抽象语法树分析,它是用来解析被测函数代码结构的,它会和预置的测试模板结合起来,构建出一个只缺少输入的被测函数单元测试代码。通过 GA (遗传算法)和 Fuzz 技术去构造出大量的测试用例输入,通过插桩(Instrumentation)技术筛选出最终用例。

工程模块

为了实现这些流程,在工程路径主要分为了三大模块:

主要模块工程要点
代码生成类似于gotests工具,要生成测试函数基本框架就要有一个代码生成的模板;SmartUnit必须对自己的构造的单元测试做断言,达到单元测试的回归验证能力;此外为了解决像网络调用的一系列依赖外部环境的问题,还需通过Mock技术屏蔽下游依赖。
数据生成为了构造足够多的被测函数输入输出,需要分析提取代码的原始语料,基于变异/组合生成更多的测试输入。
运行分析在运行阶段,要针对生成的单元测试数据做筛选。首先是要做编译编排,然后要抛弃掉无法运行的测试用例,最后要对用例去重。

项目框架

为了实现这样的工程,项目的架构平行地分为了代码的主体模块、存储架构和文件执行这几个模块。项目主体模块按执行流分为数据解析流程,代码组装流程和用例的筛选流程。

举一个工作情景方便理解:要做一个单元测试的生成的时候,运用【数据提取模块】基于历史的单元测试去做语料的生成;通过【赋值模块】将这些预料转化为真正的函数输入参数;运用【测试用例生产模块】将变异后的输入参数和代码模板本身组成了一个可以运行的测试用例,最后通过【 测试用例筛选模块】筛选出最终测试用例。

插桩原理

代码插桩基于AST将代码转换成了树形结构,使得我们可以结构化的访问代码片段,并在特定位置插入代码。 go test coverage 的官方工具也是原始代码做了一个插桩达到获取执行用例的覆盖率的效果。

以下面原始的代码块举例子:

func sum(a, b int) bool {

   if a + b > 80 {

      return true

 }

   return false

 }复制代码

【原始函数】

func sum(a, b int) (result bool) {

   branchVector := map[string]int{}

   hitMap := [3]uint32{}

   defer Save(branchVector, hitMap, result)

   hitMap[0]++

   if a+b > 80 {

      branchVector[ "branch-1" ] ++

 hitMap[2]++

      return true

 }

   hitMap[1]++

 ....复制代码

【插桩后的函数】

插桩后函数被埋入了branchVector 和 hitMap两类桩点代码。当对应的输入进入到这一个分支条件后,肯定会对这一个 map 的 key 做一赋值操作,记录下本次执行进入了这个代码块。总的来说和官方工具类似的插桩方式,使得SmartUnit了解了每一次输入在代码内部的运行状态,我们根据这种状态来评判测试用例的好坏。

函数分析

通过如下函数的用例生成流程来说明SmartUnit的分析函数的原理。下图是一个防沉迷验证函数,用户国籍是"CN"就需要查询年龄,做防沉迷验检测,否则不需要检测。

对于需要做防沉迷检测的用户来说

  1. 那么如果他年龄是在18到120 岁之间,认证为成年人,可以通过防沉迷的检测。

  2. 如果小于18 岁,认证为未成年人,不可以通过防沉迷的检测。

  3. 如果她的年龄是小于 0 岁,大于 120 岁,那么认为这一个年龄是错的。

对于这个函数,SmartUnit怎么样去生成测试用例呢?

SmartUnit通过AST语法树扫描遍历,查找到被测代码里有函数 Nationality(string) string ,用于查用户国籍。Nationality函数位于【if 表达式】 的左侧,右侧有常量"CN " 是中国的国家编码。还有"age > 18"、"age < 120" 这一些常量都会被SmartUnit识别到并提取出来用于构造对应的返回值。

【SmartUnit 构造的单元测试代码片段】

最终通过一系列的转化,SmartUnit根据分析到的数据构造出了最终测试用例。最终用例里面用mockito技术替换掉了Nationality函数的地址,并设置返回值为"CN" ,达到一个无侵入的 mock 的一个能力,原理详见golang monkey。而"age"这个参数会沿着 [18] [120] 这一些边界条件向左右两边进行数据变异,达到尽量探索代码分支的效果。

精准断言

单元测试最重要的是要对结果进行断言。SmartUnit要完成精准断言,首先需要达成一个“契约”:在接入SmartUnit时可以选定一个分支作为【基准分支】比如master分支,并认为【基准分支】所有函数的运行结果都应该是正确的(可以作为断言结果)。

SmartUnit会在【基准分支】生成测试中间代码并将执行,将本次执行结果作为断言,生成真正的测试用例。 可以简单地理解为:SmartUnit实现的断言用于判断函数的执行结果是否和【基准分支】一样。

难点突破

遗传算法 实践

SmartUnit使用遗传算法对用例进行更好的创造和筛选,该算法涉及两个重点:变异策略和筛选策略。变异策略创造更多的测试用例,适应度筛选策略用来评估最好的用例样本群体给变异策略生成子代用例。二者互为拮抗又相辅相成,帮助SmartUnit探索覆盖率最高的,覆盖分支最多的用例集合。

  • 变异策略

变异策略中我们关注两个重点:一个是怎么挑选变异初始值,另一个是怎么对这些初始值进行突变。

初始值的挑选如下图所示,SmartUnit用了非常多的分析手段去挖掘函数本身的信息,挖掘AST语法树中的面值表达式的数据是主要手段。如上文防沉迷函数的例子,挖掘到存在表达式中的"CN"常量可以大幅度减少变异次数达到获得有效输入的目标。

对于变异策略来说,按数据类型列举一些规则更方便理解:

数据类型变异策略说明
Int ,float 数值高斯分布采用高斯分布对数值类型进行左右变异。
Bool 类型关联状态反转bool类型虽然只有 true 或者 false, 但是代码中可能有很多个判断条件,独立对bool 进行变异很难完全命中 一个多条件判断的 if esle分支,这时我们就需要记录一个分支的 bool 类型组,根据这些多个条件去探索未达到的 bool组合状态。
string语料填充 + 字符串专项变异策略对于string类型的值,可以基于参数名称获取对应语料池,如在变异时获取到了 ip 的入参名称,就会以 xxx.xxx.xxx.xxx 类型的格式对字符串进行变异,同时也会考虑到抓取函数内部常量来进行赋值。
functionreflect 解析构造通过反射构造解析输入输出,并实时构造出方法。
数组复合变异 +数组专项变异涉及到数组的数据一般都要考虑代码中存在遍历语句,也有对数组长度不达标的错误处理语句,对于数组的每一个类型的值要根据。
  • 用例筛选

我们如何去筛选适应度函数,在上文【插桩原理】中说到,SmartUnit可以通过插桩来感知测试用例对应的运行状态判断用例好坏,通过插桩SmartUnit可以计算如下几种数据用于评判用例。

评价标准作用
行覆盖率以覆盖行数目为导向,虽然行覆盖率是单元测试通识标准,但不能够完全衡量代码测试用例的覆盖效果。比如说一些错误的判断,可能它只有一行两行,但它特别重要。覆盖这个分支的测试用例可能行覆盖率不高,但仍然很重要。
分支覆盖率以覆盖更多分支的为导向来评价测试用例好坏。
分支距离筛选更有可能因为参数突变而覆盖更多分支的用例。

分支距离详解

上文说到,评判单元测试用例的好坏的标准有:行覆盖率、分支覆盖率和分支距离。这三者中分支覆盖率和分支距离似乎都是以覆盖最多的代码分支作为目标导向的,但其实分支距离的标准是更“平滑”的分支覆盖率标准,它用于进一步筛选在分支覆盖率一样的测试用例中,更有可能因为参数突变而覆盖更多分支的用例,这也是遗传算法的核心部分。

在遗传算模式下,数据的产生是基于变异的,以上文防沉迷函数中 (if age > 18 )这样的一个代码块来理解分支距离的概念:在这个判断条件中, 如果age > 18 的话,代码会执行对应代码块; 当age <= 18 时就会命中另一个代码块。那么当age == 18时,根据分支距离公式它的分支距离是 0,因为age会按高斯分布平滑变异,它比别的用例更容易突变出【age > 18】【age <= 18】这两种情景的用例,。分支距离越近越容易变异出命中不同代码块的输入。

【用例集合T在某个分支上的适应度】

【用例集合T适应度函数】

其他类型的数据相似,对于分支距离不为0的数据,我们始终会在分支距离计算后则将常量K, 用于区别分支距离无穷小 和 0,因为在分支覆盖上二者有显著差异。

整体流程

SmartUnit生产流程

在SmartUnit生产流程中,分析器拆解了目标函数的调用关系图和数据语料(输入变异的初始值)。通过调用图确定被测函数要Mock的下游调用,同样解析出了被测函数的出入参数,包括 mock 函数需要的出入参数,通过算法生成合适的数据填充出入参数。

将调用关系图和数据语料,和代码模板结合,SmartUnit便生成出了可执行的被测用例。这些初始的用例合集需要进入遗传算法加持的循环迭代模式,去找寻用例集合中比较好的用例,GA 算法不断迭代会把好的测试用例“汇聚”到最终测试用例集合中。

GA模式获得了最终子代后,仍然要剔除无法编译和panic的用例才获得了最终可以运行的测试用例合集。

SmartUnit 消费流程

SmartUnit的单元测试生产流程完成生产后,有一套质量卡点流程作用于代码提交流程。凭借单元测试自动生成和精准断言,SmartUnit用一套非常强力的函数级别回归验证系统,使得SmartUnit 在质量保证流程中处在一个非常靠前的位置。

首先SmartUnit为仓库的master分支全量生成测试用例,基于 diff 更新维护测试用例。如果项目开发者想要合入master分支时,会去拉取SmartUnit测试用例出来进行执行并确认结果。如果单元测执行结果异常(断言失败,panic),说明合入分支和master分支存在着不符合预期的差异。

对于函数存在代码结构更改的,我们会在运行前做分析去除无法运行的测试用例,防止大的版本变更导致单元测试用例编译失败。

项目展示

SmartUnit可视化界面

SmartUnit 在MR流水线卡点中会拉取远端独立维护的单元测试,执行获取结果并提供报告视图。结果中可以清晰的看到SmartUnit的单元测试对项目整体的覆盖率,每个测试用例是否通过,单元测试执行详情。

点开单元测试用例可以查看覆盖率详情,其中黄色线段为SmartUnit单元测试覆盖的用例,蓝色线段为开发人员手写的用例,可见SmartUnit可以为项目带来实质性覆盖率的提高。

对于执行失败的单元测试,可以点击详情查看原因,在这个例子中我们发现了某仓库的一个函数没有做好map的并发操作,导致了数据竞争而开发人员又没有针对该函数编写测试用例,在接入了SmartUnit后为开发人员发现了后问题得以修复。

项目总结

当前SmartUnit 接入代码仓库数量接近10000个,服务于飞书、抖音、头条等多条业务线;根据统计,在某项目线运用中,SmartUnit的回归检测中为MR 拦截异常1076次占总提交的21.06%(1076/5110) ,MR 修复率 11.04%(133/1076) ;截至11.20,SmartUnit 累计检测上报BUG 9825(含静态bug检测),BUG 消费率约 19.16% (1882/9825)

加入我们,做有趣的事!

我们是质量工程的Quality Lab团队,专注前沿技术落地,提供全公司级别的工程效能产品和基础设施。公司十分重视质量和效率,我们的团队不断壮大 ,在研发智能(如SmartUnit、ByteQI、 Fastbot),工程效能(如ByteFuzz, Fatal平台),AIOps(如ByHunter, SmartEye)领域研发都有成熟产品。

目前项目都非常具有挑战性,团队非常鼓励并支持技术创新,气氛活泼有朝气。加入我们,每年有多次机会进行行业顶级会议交流,高等学府技术合作频繁;在这里你会收获 每年至少三份专利,参与顶会Paper输出,发挥空间大,成长迅速不枯燥。 无论是后端工程师还是算法工程师,我们期待你的加入:yangping.cser@bytedance.com gaoyujun@bytedance.com 。


作者:字节跳动技术质量
链接:https://juejin.cn/post/7046655577040093221


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐