找回密码
 会员注册
查看: 16|回复: 0

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

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
73707
发表于 2024-10-1 06:11:35 | 显示全部楼层 |阅读模式
本文是字节跳动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自动生成的单元测试代码的覆盖率达到29.8%,结合静态代码分析和运行捕获可以会为项目检测出1~2个 bug,捕获8 个 panic。SmartUnit目标是自动生成覆盖率达到40%的单元测试,建设全自动化代码质量保障系统,并且基于自动生成的单元测试做更多的探索和实践,如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"就需要查询年龄,做防沉迷验检测,否则不需要检测。对于需要做防沉迷检测的用户来说那么如果他年龄是在18到120 岁之间,认证为成年人,可以通过防沉迷的检测。如果小于18 岁,认证为未成年人,不可以通过防沉迷的检测。如果她的年龄是小于 0 岁,大于 120 岁,那么认为这一个年龄是错的。对于这个函数,SmartUnit怎么样去生成测试用例呢?SmartUnit通过AST语法树扫描遍历,查找到被测代码里有函数 Nationality(string) string ,用于查用户国籍。Nationality函数位于【if 表达式】的左侧,右侧有常量"CN "是中国的国家编码。还有"age > 18"、"age 18 )这样的一个代码块来理解分支距离的概念:在这个判断条件中, 如果age > 18 的话,代码会执行对应代码块;当age 18】【age
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-13 13:40 , Processed in 2.351133 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表