共计 8183 个字符,预计需要花费 21 分钟才能阅读完成。
导读: 线上零碎异样问题始终以来都是使人”谈虎色变”的,传统伎俩在解决这类问题时面临着相应的技术瓶颈。基于此,摸索基于单元测试召回异样问题的办法,实现了一套通用且无人参加的单测生成零碎,在百余模块上落地获得了肯定的成果。从近代码伎俩的单元测试着手,围绕基于单测生成技术召回异样问题的利用实际开展。次要介绍该计划 0 到 1 的整体建设思路、并从了解代码、结构高笼罩测试用例数据、生成测试用例代码以及剖析失败用例这四方面开展介绍。
引言
本文提出一种基于白盒伎俩召回异样问题的通用办法,并以 C /C++ 语言为例,介绍该办法在百度服务端的落地思路。
一 背景
线上稳定性问题始终以来是备受大家关注的。在影响产品收益或用户体验的同时,也影响着 QA 的口碑。
为了防止这类问题产生在线上,测试人员有一系列的异样测试召回伎俩能够采取,常见的有:基于压力测试、基于功能测试、基于单元测试或者基于动态扫描的异样召回伎俩。然而在足够齐备的召回能力下,还是会有问题漏出到线上,这又是为什么呢?
二 问题剖析
带着下面的疑难,本文对业界内现有异样测试伎俩在高老本、低召回这两个问题维度上进行了比照剖析,比照后果如下表 2.1 所示。
表 2.1 业界罕用异样测试伎俩毛病比照剖析
整体上看,以后的召回伎俩存在滞后性或老本高的问题,像基于压力测试的异样召回伎俩资源耗费较高,基于功能测试的异样召回伎俩除开发成本较高外,还存在异样场景不易结构等滞后性问题。基于单元测试和动态查看来发现代码问题已是这些伎俩中毛病绝对较少的,接下来更具体地比照下单元测试和动态查看这两种召回异样的伎俩。
现在,动态查看已是最常见的异样发现伎俩,其接入成本低,轻量级。以动态扫描形式来查看,不须要编译运行、不占用资源。但其存在以下问题:
- 滞后性:线上出了问题后能力转化为规定躲避同类问题复发。
- 准召低:规定靠人来设计,对某些场景会存在漏掉或者误报的问题,须要 case by case 的解决。
- 不可继续:短少围绕规定的生态建设,可能呈现规定反复开发、短少规定贡献者、规定上线后无奈无效评估等问题。
基于单元测试来召回异样问题有两个毛病:开发成本高、依赖人的意识。开发人员针对本次性能中的重要函数,编写其对应的单元测试代码来进行测试。抉择哪些函数,验证哪些异样场景都依赖开发人员的教训和被动水平。但它也有以下长处:
- 测试最小单元,易于结构数据,验证正确性
- 便于后续性能回归
- 资源耗费小
- 能更早发现问题,定位和解决成本低
通过上述剖析后,能够得出基于单元测试的劣势远大于其毛病的论断,于是大胆假如:是否最大化它的劣势,解决依赖『写』和『教训』的问题。——即主动撰写异样的单元测试代码,被动发现代码健壮性问题。
在这一构想下,提出一种可继续的、主被动联合、高 ROI 的稳定性问题召回漏斗,智能 UT 作为动态代码查看环节后的被动召回伎俩,动态分析召回问题。
图 2.1 稳定性召回漏斗图
三 解决思路
19 年初,在对生成用例代码的强诉求下,调研了 C /C++ 语言业界中比拟通用且优良的单测生成工具:C++ test 和 Wings,在开发成本、召回能力和是否开源三方面进行了比照,比照后果如下表 3.1 所示。显然,无论是 C ++ test 还是 Wings,都无奈满足业务线在简单业务场景下齐全自动化对简单类型函数生成单测代码的诉求以及扩大,因而须要自建单测代码生成能力。
表 3.1 业界 C /C++ 单测生成工具调研比照
将开发人员对一个函数撰写单元测试代码的过程进行拆解,各关键步骤如图 3.1 所示,将整个过程形象为 确认待测试函数 -> 剖析代码 -> 结构测试数据 -> 生成测试代码这 4 个过程。
- 确认待测试函数:本次提交的代码中,并非所有的变更或新增函数都须要测试,能够联合函数属性(如构造函数、析构函数等)、批改内容(如测试相干的代码、日志逻辑等无风险函数)。
- 剖析代码:汇总被测函数的代码,如参数(输出参数、外部依赖其它依赖参数)、返回值等信息。
- 结构测试数据:被动结构被测函数所需的用例数据,无需人工参加。
- 生成测试代码:被动生成测试被测函数的代码,无需人工参加。
解决思路的要害是通过代码剖析等白盒技术来实现一键异样单元测试代码的能力,实在模仿开发人员撰写单元测试代码。
图 3.1 开发人员实现单测代码编写的过程
四 实现计划
基于上一节剖析,整个技术方案设计如下图所示,本节重点介绍代码剖析、测试用例生成、代码生成能力和执行剖析的实现思路。
图 4.1 技术计划设计图
4.1 代码剖析能力
代码剖析的指标是冀望能通过动态代码扫描的伎俩,将简单的函数代码形象成结构化的函数特色数据,能够类比编译符号表。基于这份结构化数据能间接感知函数调用形式、变量申明和赋值形式等行为。
4.1.1 代码特色
C/C++ 语言中,尤其是 C ++ 这类面向对象的语言,函数调用和类的申明创立形式和一般变量不同,存在更丰盛的语法多样性。首先要明确该语言在代码剖析过程中须要取得的信息内容,重点思考的因素如下:
- 函数调用:一般函数调用、类的成员函数调用
在调用类的一般成员函数前,须要先实例化类的对象,而非成员函数可间接调用。
- 变量申明实现:一般变量、class 或 struct 变量、stl 变量等
不同变量申明赋值形式都不同,须要可能辨别是一般变量还是 class、struct、stl 变量
- 修饰符:const、static、virtual、inline 等
加了修饰符的变量或函数会影响它的调用、实例化、赋值形式
- 文件级别信息:头文件,命名空间
头文件和命名空间不全或者缺失会影响测试代码的编译
- 其它
一些影响赋值、实例化语法的其它属性。如类是否禁用了拷贝 / 赋值构造函数等。
基于上述思路,最后敲定获取如下代码特色信息:
表 4.1 代码特色信息
4.1.2 特色存储
将特色存储以 xml 文件格式存储,存储为代码构造数据(CodeStructData,CSD),且保障周边模块能基于该份产出获取函数调用形式、变量申明和赋值形式。依据不同类型和赋值形式约定 schema,如 type、baseType1、parmType 等属性,Demo 如下图所示。
- type:理论类型
- baseType1:该变量理论属于类别,如内置类型、数组类型、STL 类型等。
- parmType:申明类型,生成代码时可间接取该字段作为变量的申明类型。
![]
图 4.2 经源码剖析失去的 CSD 样例
4.1.3 特色采集
这一环节,冀望能在不编译的条件下,以动态代码扫描形式提取代码信息,且工具要轻量、高效、反对开源,以便于后续需要迭代。
在综合比照下,最终抉择 cppcheck,一个开源的动态代码查看工具,除此之外还能够基于它的符号表来做二次开发。为了采集函数调用链信息和其它全局信息,外部对 cppcheck 进行了革新,后续会独自介绍,本文不多赘述。采集过程如下图 4.3 所示。
图 4.3 基于 cppcheck 的代码剖析计划示意图
4.2 用例数据生成能力
4.2.1 解决思路
用例数据生成能力属于 Fuzzing 技术畛域中要害的一个环节,常见的 fuzz 数据伎俩有基于生成的和基于变异的两种形式。个别会应用覆盖率来掂量 fuzz 能力,比方函数笼罩、行笼罩或分支笼罩。
- 基于变异法:依据已知数据样本通过变异的办法,生成测试用例。比方驰名的 AFL-fuzz 技术,其次要处理过程如下图 4.4 所示:
图 4.4 AFL-Fuzz 处理过程
- 基于生成法:依据已知协定或接口标准进行建模,生成测试用例。比方 libfuzzer 能够在不指定初始数据集下,通过被测指标的接口类型,随机生成字节数据,喂给被测指标。
在生成用例数据时,防止用例爆炸也是生成的条件之一,过多的用例会存在用例有效和运行时效低问题。
本文在传统的基于生成法结构用例数据的根底上,除了被测指标接口协议外,充分利用门路和分支信息来领导 fuzz 数据,笼罩更多分支内的逻辑,还引入了其它白盒特色,如变量扩散关联性等去升高对有效用例的生成,最初以函数笼罩和分支笼罩作为 fuzz 能力的度量指标。
解决思路如下图 4.5 所示,数据生成层由 CSD 解决模块、门路抉择模块、参数抉择模块和生成 & 筛选模块形成。针对不同类型的变量,选取不同的异样候选集,生成初始用例汇合,再通过用例筛选策略失去最终的测试用例集。
图 4.5 测试用例生成计划示意图
4.2.2 门路抉择
门路抉择模块蕴含表达式束缚求解、门路可达剖析以及门路合并。这一部分的目标是领导数据生成对分支的笼罩。门路的提取,次要通过遍历上一节提取的程序控制流数据来实现,能够采取深度优化遍历或广度优先遍历,不影响后果。
为了防止门路爆炸,能够先提取出冀望测试笼罩的指标,遍历时每次抉择一个能够笼罩待测试指标的门路。
1)束缚求解是指对门路上的分支表达式进行求解计算,别离计算出表达式为真和假时的符号值。这里须要先对表达式进行替换,例如将函数调用替换成变量,便于计算。替换后的表达式能够应用开源的库进行求解,如 z3。
2)门路可达剖析是指以分支如 if、while、for、switch 为节点,计算节点内求解出的变量值或变量范畴,对函数外部各节点进行连贯后,失去一个图。联合每个节点变量的范畴,对图中的门路进行剔除,删掉不可达的门路。
3)门路合并是指将含有交加的节点合并成一条门路,缩小后续用例生成数量。如下图 4.6 中对_index_i 和_index_j 结构用例时,结构出 {_index_i=1, _index_j=2} 来满足同时笼罩 17 行和 22 行两个分支的数据。在解决时须要剖析出分支外部是否存在 return、continue、break 这类的跳转或返回关键字,避免出现 badcase。
图 4.6 程序示例
4.2.3 候选数据源
各类型的候选异样数据可分为静态数据和动态数据。
- 静态数据指通过历史教训保护的一份类型边界值和业务边界值数据库。
- 动态数据指通过业务数据采集和变异算法,基于模块日志、流量等数据源通过插桩的形式挖掘出的业务值或通过变异失去的异样边界值。
4.2.4 用例生成 & 筛选
基于上述步骤失去各参数候选值汇合后,便可对参数之间进行组合,失去用例汇合,参数组合的形式间接影响着用例量级,此阶段重点思考如何防止用例爆炸,减量不减品质。
经统计,大概 70% 以上的软件问题是由一个或2个参数作用引起的。因而参数因子两两组合就成为了软件测试中一种施行性较强同时又比拟无效的办法。如果采纳全排列组合形式,在某业务场景下,某类 classA 类型作为函数形参,假如该 classA 有 1000 个成员变量,其成员变量全副为 v 类型,类型 v 有 4 个取值,v=[-1,0,1,-2147483649],那么全排列组合后的用例数据量高达 4^1000 个。
可见,单纯的全排列组合能保障以后两两因子组合笼罩的场景最丰盛,但会面临 case 爆炸问题,这不符合实际利用背景。
其实,生成一个最小测试用例集是一个 NPC 问题,因而学术界个别是将找到一个尽可能小的测试用例集去笼罩所有可能的配对来作为钻研指标。本文先后应用两个步骤来加重用例的量级。
- 剔除无用属性:基于代码剖析缩小对无用属性的数据结构。
通过剖析自定义类型参数其成员属性扩散性,只对类 / 构造体中理论被用到的成员属性结构数据。
图 4.7 函数内变量和其成员变量样例
- 剔除冗余用例:采纳基于生成的形式,抉择一种参数组合算法,生成适合的测试用例。常见的生成技术大体可分为组合设计法、启发式算法、元启发摸索法。
- 组合设计法:个别是围绕正交表或其它代数的思路生成测试用例。
- 启发式算法:个别是逐条地或逐因素扩大地生成测试用例。如经典的 AETG 算法:首先按贪婪算法生成肯定数量N个测试用例,而后从这N个测试用例中抉择一个能最多笼罩未笼罩配对汇合中参数对的用例,将这个用例增加进曾经造成的测试用例集T中,直至达到笼罩指标。如 IPO 算法,通过先程度、再垂直的形式裁减用例。
- 元启发式算法:如遗传算法、模拟退火、蚁群算法等,大抵过程如下图 4.8 所示。
图 4.8 元启发式用例摸索大抵过程
启发式和元启发式都属于部分搜索算法,不能保障最优,但能够保障解决工夫。还能够将逐条生成法和元启发形式联合,引入谬误风险系数、组合束缚和参数优先级等信息,进一步优化组合形式。
本文初期利用逐条生成的形式,基于根底的成对法来缩小反复有效的输出。以一个例子简略介绍本文应用的 2 -Wise testing 成对法思路(其原理可参考文末提供的材料):
假设有三个输出变量,X、Y、Z,取值别离为 D(X)={x1,x2,x3},D(Y)={y1,y2},D(Z)={z1,z2};
如果用全排列法,失去的测试用例集有 3 X 2 X 2 = 12 个用例,具体测试用例如下左图 4.9 所示,通过 2 -Wise testing 解决后仅取得 6 个用例。
图 4.9 全排列用例和成对法算法过程
本文通过上述办法,无效剔除了 90% 以上的无用测试用例数据。最终将保留下来的测试用例以 json 格局存储,作为测试数据汇合,不便扩大和供其它场景应用。数据 Demo 如下图 4.10 所示,以函数名、func_data、变量名作为 key,以具体的参数值作为 value。
图 4.10 测试用例汇合 demo 图
以后这种生成形式是以参数和参数之间互相独立为假如前提,思路简略,而理论业务场景下,参数和参数之间是可能存在关联的,在生成形式上还有较大晋升空间,前期会在以后逐条生成的能力下,引入元启发摸索算法,如在该畛域成果比较显著的遗传算法或模拟退火算法,在每生成一条测试用例时都调用摸索算法,以晋升覆盖率和重要笼罩元素为指标,生成无效测试用例集,这也是智能 UT 中的最重要的『智能』场景之一,数据是揭错之本。
4.3 代码生成能力
4.3.1 解决思路
代码生成畛域目前次要有两个重要方向:程序生成和代码补全。生成测试代码属于程序生成方向,采纳深度学习算法生成代码是目前学术界以后比拟重要的钻研方向,曾经基于一些开源的代码作为语料库获得了肯定的技术冲破,但因存在泛化能力弱的问题,还无奈在工业界落地。
在理论技术落地中,程序生成的正确性间接影响测试工作的稳定性,思考到这一束缚,本文目前采纳基于语法规定和模板的生成形式来生成测试用例代码。语法规定和代码构造数据正确即可保障生成代码语法正确,达到生成即可编译的指标。
具体实现计划参考如下图 4.11 所示,将上述步骤失去的代码构造数据和测试用例汇合数据下发给代码生成解决模块,模块通过管制层抉择不同语言对应的生成器,再依据不同类型抉择对应的生成算子。对于可变内容,深度遍历代码构造数据的每一个函数节点、参数节点和全局节点,针对各自节点下的代码信息,获取对应的语法适配生成算子来生成指标代码,从而失去测试用例代码,再联合模板中的固定源码,封装成可编译运行的单测代码。这一过程能够类比编译器联合语法树生成指标代码的过程。
像 C /C++ 语言,生成基于 Gtest 的死亡测试封装的测试用例代码,测试被测函数是否非预期死亡。还能够基于以后的生成框架,便捷地扩大其它语法规定来生成不同语言不同模式的用例代码。
图 4.11 代码生成计划示意图
4.3.2 残缺 demo 展现
如下图所示是一个被测源码 exlore_filter 函数通过代码剖析、用例数据生成后失去测试用例代码的过程样例。
图 4.12 测试用例汇合 demo 图
4.4 失败用例剖析
基于上一节介绍的代码生成能力,可取得可编译的测试用例代码,通过编译适配模块生成编译命令,执行编译后即可失去可执行的测试程序。如何保障运行测试程序后疾速获取失败信息,升高人染指的剖析老本,是本节重点介绍的内容。
整个剖析过程中可能存在的问题如下:
- 可读性差:测试用例失败后,其堆栈 /crash 不残缺,或者无用信息太多。像 c /c++ 语言的 gtest 死亡测试,用例 crash 后是无堆栈信息打印进去的,惯例形式是通过 gdb 来获取堆栈内容,当堆栈文件过大超过 3G 时,读取速度会很慢。
- 反复的堆栈 /crash
- 同一函数同一代码行问题反复,次要是不同用例之间命中的问题反复。
例如如下场景的 find 函数,在输出用例为 {arr=nullptr,len=1} 和{arr=nullptr,len=2}时都会命中 sum+=arr[0]这一行的 crash。
图 4.13 被测代码片段
- 不同代码行问题反复,次要是代码语义相近导致的问题反复
例如如下场景的两个程序片段 A 和程序片段 B,_dest 是类 Action 的成员变量,会在程序运行的其它阶段被赋值。add_to_dest 和 get_from_dest 别离 crash 在 write_dest->write 和 read_dest->read 行。其代码行内容是不同的,但 crash 的语义是雷同的,都是应用了空指针_dest 导致程序 crash。
图 4.14 被测代码片段 A
图 4.15 被测代码片段 B
- 定位老本高
对新人或不相熟堆栈文件的人来说,测试用例代码、CR 以及堆栈信息都齐备的状况下,仍然存在跟进排查无脉络的问题。
- 修复规范不对立
哪些问题肯定要修复、哪些问题能够疏忽掉,不同业务线短少对立的规范。
本文通过堆栈内容存储、堆栈内容分析、去重、失败起因预测以及失败问题分级等伎俩来解决上述问题,解决思路如下图所示,每个阶段细节较多,本文不重点开展介绍。
图 4.16 堆栈剖析过程
4.5 技术架构
面向业务落地须要思考如何将工具的能力施展到适合的阶段,做到恰到好处,联合研发开发习惯,咱们思考了如下两方面因素:
- 存量问题须要修复周期:业务模块间接扫描,会因历史遗留问题过多而产生较大修复老本,须要肯定的工夫来消化。
- 迭代时只须要关注变更影响面:在变更流水线上扫描全量代码,对全量代码生成用例会造成资源节约以及执行效率低的问题。
基于上述思考,咱们将落地形式划分为两种模式:存量和增量。
- 存量:新接入模块倡议先跑全量,扫描存量问题,让研发团队出对立修复负责人,进行对立修复,打消存量隐患。也能够在 daily 工作或全量回归工作中跑存量扫描模式。
- 增量:是指只针对变更代码,通过白盒剖析伎俩,剖析出其影响的代码范畴,如间接影响(改变函数)、间接影响(未改变但逻辑上会有影响),只对影响范畴内的函数进行测试。在批改代码提交后,可触发流水线跑增量模式的工作。
这里还能够引入危险考量,评估出函数批改内容是否须要测试,剔除掉无风险函数。
基于上述思路,将代码剖析、用例数据生成和代码生成能力集成到如下技术架构中,和百度外部策略中台、数据中台、可视化平台等能力联合,贯彻测试筹备、测试执行、测试剖析到问题定位这四个维度,实现基于单测生成的异样召回工具的建设和落地。
图 4.17 落地架构图
局部工作后果如下图所示,研发人员本地开发提交代码后主动触发流水线绑定的智能 UT 测试工作,通过报告可查看到 crash 问题详情,包含失败起因、失败堆栈内容等。
图 4.18 工作执行展现样例图
五 成果
1. 工程成果
- 实际:摸索出基于单测解决异样问题的通用计划,已在 C /C++ 语言上落地实际,累计生成千万余行测试代码,其它语言进行中
- 高笼罩:冷启函数笼罩 50%+,分支笼罩 20%+
- 低资源:机器资源耗费同零碎级测试相比可疏忽
- 低人耗:主动适配 UT 及测试代码编译能力,无需人工搭建单测框架和保护
2. 业务成果
- 落地:笼罩 140+ 重点后端模块、lib 库
- 存量召回:召回存量问题 900 余例
- 增量召回:增量召回问题 200 余例
参考资料
1.cppcheck:https://github.com/danmar/cpp…
2.Fuzzing:https://baike.baidu-com/item/…
3.z3:https://github.com/Z3Prover/z3
4.all-pairs_testing: https://en.wikipedia.org/wiki…
5. 死亡测试:https://github.com/google/goo…
6.traceback:实现思路参考 https://github.com/zsummer/tr…
7.address sanitizer:https://github.com/google/san…
———- END ———-
百度架构师
百度官网技术公众号上线啦!
技术干货 · 行业资讯 · 线上沙龙 · 行业大会
招聘信息 · 内推信息 · 技术书籍 · 百度周边
欢送各位同学关注!