长篇详细阐述:基于代码的测试生成技术在召回异常问题中的应用实践
The following article is from 百度智能化测试 Author 车厘子和夹心饼干
滞后性:线上出了问题后才能转化为规则规避同类问题复发。 准召低:规则靠人来设计,对某些场景会存在漏掉或者误报的问题,需要case by case的解决。 不可持续:缺少围绕规则的生态建设,可能出现规则重复开发、缺少规则贡献者、规则上线后无法有效评估等问题。
测试最小单元,易于构造数据,验证正确性 便于后续功能回归 资源消耗小 能更早发现问题,定位和解决成本低
确认待测试函数:本次提交的代码中,并非所有的变更或新增函数都需要测试,可以结合函数属性(如构造函数、析构函数等)、修改内容(如测试相关的代码、日志逻辑等无风险函数)。 分析代码:汇总被测函数的代码,如参数(输入参数、内部依赖其它依赖参数)、返回值等信息。 构造测试数据:主动构造被测函数所需的用例数据,无需人工参与。 生成测试代码:主动生成测试被测函数的代码,无需人工参与。
函数调用:普通函数调用、类的成员函数调用
变量声明实现:普通变量、class或struct变量、stl变量等
不同变量声明赋值方式都不同,需要能够区分是普通变量还是class、struct、stl变量
修饰符:const、static、virtual、inline等
加了修饰符的变量或函数会影响它的调用、实例化、赋值方式
文件级别信息:头文件,命名空间
头文件和命名空间不全或者缺失会影响测试代码的编译
其它
一些影响赋值、实例化语法的其它属性。如类是否禁用了拷贝/赋值构造函数等。
基于上述思路,最初敲定获取如下代码特征信息:
type:实际类型 baseType1:该变量实际属于类别,如内置类型、数组类型、STL类型等。 parmType:声明类型,生成代码时可直接取该字段作为变量的声明类型。 3.1 经源码分析得到的CSD配置样例
3.2 用例数据生成能力
3.2.1 解决思路
基于变异法:根据已知数据样本通过变异的方法,生成测试用例。比如著名的AFL-fuzz技术,其主要处理过程如下图3.3所示:
基于生成法:根据已知协议或接口规范进行建模,生成测试用例。比如libfuzzer可以在不指定初始数据集下,通过被测目标的接口类型,随机生成字节数据,喂给被测目标。
3.2.2 路径选择
路径选择模块包含表达式约束求解、路径可达分析以及路径合并。这一部分的目的是指导数据生成对分支的覆盖。路径的提取,主要通过遍历上一节提取的程序控制流数据来完成,可以采取深度优化遍历或广度优先遍历,不影响结果。
为了避免路径爆炸,可以先提取出期望测试覆盖的目标,遍历时每次选择一个可以覆盖待测试目标的路径。
约束求解是指对路径上的分支表达式进行求解计算,分别计算出表达式为真和假时的符号值。这里需要先对表达式进行替换,例如将函数调用替换成变量,便于计算。替换后的表达式可以使用开源的库进行求解,如z3。
路径可达分析是指以分支如if、while、for、switch为节点,计算节点内求解出的变量值或变量范围,对函数内部各节点进行连接后,得到一个图。结合每个节点变量的范围,对图中的路径进行剔除,删掉不可达的路径。
路径合并是指将含有交集的节点合并成一条路径,减少后续用例生成数量。如3.1程序代码示例中对_index_i和_index_j构造用例时,构造出{_index_i=1, _index_j=2}来满足同时覆盖17行和22行两个分支的数据。在处理时需要分析出分支内部是否存在return、continue、break这类的跳转或返回关键字,避免出现badcase。
3.1 程序代码示例
3.2.3 候选数据源
各类型的候选异常数据可分为静态数据和动态数据。
静态数据指通过历史经验维护的一份类型边界值和业务边界值数据库。 动态数据指通过业务数据采集和变异算法,基于模块日志、流量等数据源通过插桩的方式挖掘出的业务值或经过变异得到的异常边界值。
3.2.4 用例生成&筛选
组合设计法,一般是围绕正交表或其它代数的思路生成测试用例。 启发式算法,一般是逐条地或逐因素扩展地生成测试用例。如经典的AETG算法:首先按贪心算法生成一定数量N个测试用例,然后从这N个测试用例中选择一个能最多覆盖未覆盖配对集合中参数对的用例,将这个用例添加进已经形成的测试用例集T中,直至达到覆盖目标。如IPO算法,通过先水平、再垂直的方式扩充用例。
元启发式算法,如遗传算法、模拟退火、蚁群算法等,大致过程如下图3.5所示。
本文通过上述方法,有效剔除了90%以上的无用测试用例数据。最终将保留下来的测试用例以json格式存储,作为测试数据集合,方便扩展和供其它场景使用。数据Demo如下所示,以函数名、func_data、变量名作为key,以具体的参数值作为value。
3.3 代码生成能力
代码生成领域目前主要有两个重要方向:程序生成和代码补全。生成测试代码属于程序生成方向,采用深度学习算法生成代码是目前学术界当前比较重要的研究方向,已经基于一些开源的代码作为语料库取得了一定的技术突破,但因存在泛化能力弱的问题,还无法在工业界落地。
在实际技术落地中,程序生成的正确性直接影响测试任务的稳定性,考虑到这一约束,本文目前采用基于语法规则和模板的生成方式来生成测试用例代码。语法规则和代码结构数据正确即可保证生成代码语法正确,达到生成即可编译的目标。
具体实现方案参考如下图3.7所示,将上述步骤得到的代码结构数据和测试用例集合数据下发给代码生成处理模块,模块通过控制层选择不同语言对应的生成器,再根据不同类型选择对应的生成算子。对于可变内容,深度遍历代码结构数据的每一个函数节点、参数节点和全局节点,针对各自节点下的代码信息,获取对应的语法适配生成算子来生成目标代码,从而得到测试用例代码,再结合模板中的固定源码,封装成可编译运行的单测代码。这一过程可以类比编译器结合语法树生成目标代码的过程。
像C/C++语言,生成基于Gtest的死亡测试封装的测试用例代码,测试被测函数是否非预期死亡。还可以基于当前的生成框架,便捷地扩展其它语法规则来生成不同语言不同形式的用例代码。
4.3.2 完整demo展示
如图3.8所示是一个被测源码exlore_filter函数经过代码分析、用例数据生成后得到测试用例代码的过程样例。
4.4 失败用例分析
可读性差:测试用例失败后,其堆栈/crash不完整,或者无用信息太多。像c/c++语言的gtest死亡测试,用例crash后是无堆栈信息打印出来的,常规方式是通过gdb来获取堆栈内容,当堆栈文件过大超过3G时,读取速度会很慢。 重复的堆栈/crash
同一函数同一代码行问题重复,主要是不同用例之间命中的问题重复
例如,如下场景的find函数,在输入用例为{arr=nullptr,len=1}和{arr=nullptr,len=2}时都会命中sum+=arr[0]这一行的crash。
不同代码行问题重复,主要是代码语义相近导致的问题重复
例如如下场景的两个程序片段A和程序片段B,_dest是类Action的成员变量,会在程序运行的其它阶段被赋值。add_to_dest和get_from_dest分别crash在write_dest->write和read_dest->read行。其代码行内容是不同的,但crash的语义是相同的,都是使用了空指针_dest导致程序crash。
本文通过堆栈内容存储、堆栈内容分析、去重、失败原因预测以及失败问题分级等手段来解决上述问题,解决思路如图3.89所示,每个阶段细节较多,本文不重点展开介绍。
3.5 技术架构
存量问题需要修复周期:业务模块直接扫描,会因历史遗留问题过多而产生较大修复成本,需要一定的时间来消化。 迭代时只需要关注变更影响面:在变更流水线上扫描全量代码,对全量代码生成用例会造成资源浪费以及执行效率低的问题。
存量:新接入模块建议先跑全量,扫描存量问题,让研发团队出统一修复负责人,进行统一修复,消除存量隐患。也可以在daily任务或全量回归任务中跑存量扫描模式。 增量:是指只针对变更代码,通过白盒分析手段,分析出其影响的代码范围,如直接影响(改动函数)、间接影响(未改动但逻辑上会有影响),只对影响范围内的函数进行测试。在修改代码提交后,可触发流水线跑增量模式的任务。
基于上述思路,将代码分析、用例数据生成和代码生成能力集成到 如图3.910所示的技术架构中,和百度内部策略中台、数据中台、可视化平台等能力结合,贯彻 “测试准备、测试执行、测试分析到问题定位” 这四个维度,完成基于单测生成的异常召回工具的建设和落地。
高覆盖:冷启函数覆盖50%+,分支覆盖20%+ 低资源:机器资源消耗同系统级测试相比可忽略 低人耗:自动适配UT及测试代码编译能力,无需人工搭建单测框架和维护
落地:覆盖140+重点后端模块、lib库 存量召回:召回存量问题900余例 增量召回:增量召回问题200余例
参考资料 1. cppcheck:https://github.com/danmar/cppcheck 2.Fuzzing:https://baike.baidu-com/item/%E6%A8%A1%E7%B3%8A%E6%B5%8B%E8%AF%95/2848962?fr=aladdin 3. z3:https://github.com/Z3Prover/z3 4. all-pairs_testing: https://en.wikipedia.org/wiki/All-pairs_testing 5. 死亡测试:https://github.com/google/googletest/blob/master/googletest/docs/advanced.md#death-tests 6. traceback:实现思路参考https://github.com/zsummer/traceback 7. address sanitizer:https://github.com/google/sanitizers/tree/master/address-sanitizer