μBERT:基于预训练语言模型的变异测试

互联不一般哥 2024-06-04 11:52:41

引用

Degiovanni R, Papadakis M. µbert: Mutation testing using pre-trained language models[C]//2022 IEEE International Conference on Software Testing, Verification and Validation Workshops. IEEE, 2022: 160-169.

论文:μBERT: Mutation Testing using Pre-Trained Language Models

摘要

本文介绍了一种名为µBERT的突变测试工具,它使用预训练的语言模型(CodeBERT)通过掩盖和替换标记来生成突变体。µBERT将突变测试和自然语言处理相结合,形成自然的突变体。与其他研究不同,µBERT直接生成突变体,而不依赖于基于语法的突变操作符。该工具通过使用大型代码训练的语言模型来捕捉突变体的自然性。文章还介绍了µBERT在故障检测和断言推理两个应用场景中的初步评估结果,并展示了µBERT生成的突变体与传统突变的区别。

1 引言

变异测试使用一组预定义的简单语法转换,即变异操作符,基于目标编程语言的语法定义缺陷。因此,变异操作符经常以经常导致非自然代码的方式改变程序语义 (非自然的意义在于,突变的代码不太可能由有能力的程序员产生)。

这种不自然的错误可能对开发人员没有说服力,因为他们可能会认为它们是不现实/无趣的,从而阻碍了方法的可用性。此外,非自然突变体的使用可能会对突变测试的指导和评估能力产生实际影响。这是因为非自然突变体往往会导致异常,或分割错误、无限循环等琐碎的情况。

为了解决这个问题,我们提出形成某种意义上自然的突变体;意思是变异后的代码/语句遵循有能力的程序员所产生的代码的隐含规则、编码约定和一般代表性。我们使用在大代码上训练的语言模型来定义/捕获这种变异体的自然性, 这些语言模型在给定其周围代码的情况下学习(量化)代码标记的发生。

我们提出了µBERT,这是一种突变测试工具,它使用预训练的语言模型(CodeBERT)通过屏蔽和替换令牌来生成突变。µBERT结合突变测试和自然语言处理,形成自然突变体。与目前针对突变体选择的研究不同,µBERT不依赖任何基于句法的突变算子,直接生成突变体。这种方法进一步吸引人,因为它简化了突变体的创建并限制了突变体的数量。

虽然,有许多方法可以通过考虑突变体的位置及其影响来调整µBERT,但在我们的初步分析中,我们以暴力方式播种故障,类似于突变测试,通过迭代每个程序语句并屏蔽每个涉及的令牌。具体来说,我们进行了以下步骤:

(1) 根据被分析的表达式类型,一次选择和掩码一个token;

(2) 用掩码序列馈送CodeBERT,得到预测结果;

(3) 通过将掩码token替换为预测token来创建变异体;

(4) 丢弃不可编译和重复的变异体。

为了展示µBERT的潜力,我们对以下两个用例进行了初步评估:

故障检测:我们专注于突变测试场景,并分析了用于杀死µBERT突变体的套件的故障检测能力,并将其与流行的突变测试工具(即PiTest)进行了比较。我们考虑了来自缺陷4j 的总共40个bug,用于3个项目,即Cli、Collections和Csv。我们的结果表明,µBERT引导的测试套件发现了40个bug中 的27个,而PiTest的突变体帮助发现了40个bug中的26个。µ B ERT发现的bug中有3个没有被PiTest发现,而PiTest发现的 bug中有2个没有被µBERT发现。此外,我们表明µB ERT(高达 100%)比PiTest更具成本效益。断言推断:我们研究了µB ERT突变体在程序断言推断技术中的实用性,该技术使用突变体对候选序列进行排序和丢弃(通常,杀死更多突变体的断言是首选,而不杀死任何突变体的断言被丢弃)。特别地,我们关注了最近在中报道的4个传统突变测试表现不佳的案例。我们发现µBERT可以补充和贡献有趣的突变体,而不是帮助改善推断出的断言的质量。最后,我们展示了由µBERT产生的具有有趣性质的突变体的示例,展示了它们与传统突变的差异。

2 技术介绍

µBERT是一种自动化方法,它使用预训练的语言模型 (即CodeBERT)来生成java程序的突变体。

图1描述了µBERT的工作流程,可以总结如下:

1) µBERT首先解析作为输入的Java类,并提取候选表达式进行变异。

2) 突变操作符分析和屏蔽每个java表达式感兴趣的标记(例如,二进制表达式突变将屏蔽二进制操作符),然后调用 CodeBERT来预测被屏蔽的标记。µBERT将尝试为 CodeBERT提供序列,这些序列涵盖了所分析表达式的尽可能多的周围上下文(最多512个令牌)。

3) µBERT接受CodeBERT预测,并通过用预测的标记替换被屏蔽的标记来生成突变体(每个被屏蔽的表达式创建5个突变体)。

4) 最后,不能编译或语法与原始程序相同的突变(CodeBERT预测原始掩码令牌的情况)将被丢弃。

图1 核心框架图

我们的原型实现支持各种各样的Java表达式,能够改变一元/二进制表达式、赋值语句、字面量、变量名、方法调用、对象字段访问等等。这表明,对于同一个程序位置,可以生成多个变异体。例如,对于像a + b这样的二进制表达式,µbERT将从以下3个掩码序列创建(可能有15个)突变: + b, a b和a + 。下面我们提供了一些例子,展示了µBERT支持的不同突变操作符。

A. 二元表达式突变: 给定e =<exp><op><exp> ,程序P中方法M的二进制表达式,其中<exp>和<op>分别表示Java表达式和二进制操作符,µBERT创建一个新表达式e^0=<exp><mask> 通过用特殊标记<mask>替换(屏蔽)二进制操作符<op>来创建(屏蔽)一个二进制操作符<op><exp>。然后,一个新的方法M 0 = M [e←e^0 ]被创造出来,它看起来和M一模一样,但表达式e被掩码表达式e取代0。µBERT调用CodeBERT方法m0中最大的代码序列,包括e^0和,不超过最大序列长度(512个令牌)。CodeBERT返回一个包含5个预测标记的集合(t1,…),t5 )。因此,µBERT产生5个突变体,即P1,…, P5,使得每个突变体Pi用预测的一个ti取代突变的算子<op>

图2显示了µBERT可以为二进制表达式生成的突变体的一个示例。函数isLeapYear如果输入的日历年是闰年,则返回true。要改变的二进制表达式之一是e: year % 4。为 此,µB ERT掩码二进制运算符%,导致掩码表达式e0: year 4。整个掩码方法用于提供CodeBERT,它预测以下5个标记:t1: ' % ', t2: ' / ', t3: ' % ', t4: ' - '和t5: ' / '。首先注意token t1和t3只在一个空格上不同,并且与原始token重合,因此这些变异体将被丢弃。第二,token t2和t5是一样的,除了t5中额外的空格,所以只会用一个来生成突变体。最后,µBERT产生2个可编译的突变体,基于表达式 e2: year / 4和e4: year - 4 。

图2

B.一元表达式突变: 在处理一元表达式时,µB ERT区分两种情况,这取决于操作符是出现在表达式之前还是之后(例如++x和x——)。 为简单起见,考虑e =<op><exp>是mu-tate的一元表达式。 然后,µBERT将掩码运算符令牌<op>,导致掩码表达式 e0 =<mask><exp>,然后将掩码序列送入CodeBERT。µB ERT需要CodeBERT预测(t1,…,t5 )并创造突变体P1,…, P5通过将一元运算符替换为预测tokenti。也就是说,Pi= P [e←ei ],ei=ti< exp >∈(1 . . 5)。重复的、语法相同的、不可编译的变异体最终被丢弃。

图3

图3显示了µBERT可以为一元表达式生成的突变体示例。函数printArray以逆序打印出作为输入给出的数组arr的元素。 考虑µB ERT将改变一元表达式e:——i,为此它生成掩码表达式e0 :<mask> i,并将其输入CodeBERT。µBERT接收以下预测:t1: ' ++ ', t2: '——',t3: '——',t4 :' ++ '和t5 :' ! '。µB ERT丢弃与原始语法相同的突变体(标记t2和t3 )),并考虑两 个候选突变体(t1和t5 )),但只编译突变t1 (获得e1: ++i)。

C. 字面量和变量名突变: 这种变异很简单。为了简单起见,考虑表达式e = <cons>to mutatea文字(常数)。µBERT从屏蔽e开始,导致e^0=<mask> ,用于馈送CodeBERT。µBERT创建突变体P1,…, P5通过用预测的token替换突变的字面名称(即Pi= P[e←ti ]for i∈[1..5])。 再次考虑图2中的函数isLeapYear,其中文字表达式e: 4 是要改变的表达式(从年份% 4开始)。用掩码记号替换e后, CodeBERT返回以下5个预测:t1 :' 4 ',t2 :' 100 ',t3: ' 400 ',t4: ' 10 '和t5: ' 2 '。请注意,token t2和t3出现在突变表达式的上下文中。还要注意,第一个预测(t1 )与原始token重合,因此被丢弃。最后,µBERT返回4个可编译的突变体,通过用预测的令牌t2,t3,t4和t5替换掩码令牌生成。

D. 更多的突变算子:µBERT还可以改变赋值、方法调用、对象字段访问、 数组读写和引用类型表达式。下面我们提供了µB ERT产生的屏蔽序列的例子,以突变这些类型的表达。遵循前面已经描述的相同过程,µBERT将通过用CodeBERT预测替换掩码令牌来生成突变体。请注意,所显示的预测是在我们的实验中观察到的,但如果在不同的环境下进行评估,这些预测可能会发生变化。

对于像avg += it_result这样的赋值表达式,µB ERT产生掩码 表达式avg = it_result。典型的CodeBERT预测 是+,-,*和/导致潜在的可编译突变,例如,avg -= it_ result。在方法调用表达式中,如图4中的children.add(c),µB ERT 掩码方法名,产生子函数(c)。CodeBERT预测了以下方法名:add、addAll、push、remove和added。µBERT同样丢弃相同和不可编译的突变体,得到两个突变体:children. push(c)和children.remove(c)。在访问特定对象字段的表达式中,µBERT屏蔽对象字段名。例如,对于类似list的表达式。head = new_node,µBERT生成掩码表达式列表。<掩码> = new_node。Code-BERT预测我们通 常会得到cover head, next, tail, last和first。在数组读(和/或写)表达式中,µBERT掩码用于访问数组的整个索引。例如

图4

对于图5中的表达arr[mid-1],µBERT产生arr[]被屏蔽的表达。然后,CodeBERT预测值为0,n, mid, 1和low,允许µBERT生成5个可编译的突变体(变量n, low和mid存 在于上下文中)。值得注意的是,数组名(arr)和索引表达式mid - 1将分别被变量名变异操作符和二进制表达式变异操作符变异。

图5

在引用某种类型的表达式中,例如int number = (int)(Math. random() * 10),µBERT掩码所引用类型的类名。在这种情况下,µBERT产生掩码表达式int number = (int)(. random() * 10)。对于这个例子,我们得到的预测引用了Math、 random、random 和 System, 导致了int number = (int)(random . random() * 10)这样的变异体。

3 实验评估

3.1 研究问题

我们通过研究用于杀死µBERT突变体的测试套件的故障检测能力开始我们的分析。因此,我们提出:

(RQ1) µBERT产生的突变体在检测实际故障方面的效果如何? 在故障检测方面,µBERT与PiTest相比如何?

为了回答这个问题,我们评估了选择的测试套件的故障检测能力,以杀死µBERT和PiTest(我们的基线)产生的突变体。故障检测能力通过使用一组取自Defects4J的真实故障来近似。

变异测试的另一个应用案例是程序断言的生成。特别地,利用变异测试通过程序断言推理技术进行断言的选择和丢弃。鉴于此,我们提出:

(RQ2)µBERT是否成功地选择了“好的”断言?它和PiTest相比怎么样?

为了回答这个问题,我们使用了一个由手动编写的断言(ground-truth)组成的数据集,该数据集最近被用于评估SpecFuzzer工具,这是一种最先进的规范推理技术。特别地,我们选择了4个手动编写的断言,这些断言被SpecFuzzer错误地丢弃了,因为它们没有杀死任何突变体。 因此,我们研究了µBERT是否可以帮助选择这些断言,并将其与PiTest进行比较。

最后,我们对µBERT产生的一些突变体进行定性分析,并提出以下问题:

(RQ3) 与传统的突变检测操作相比,µBERT产生的突变是否不同?

我们展示了由µBERT生成的突变体,这些突变体有助于检测PiTest未发现的故障,以及帮助SpecFuzzer从基本事实中保存断言的突变体,这些突变体被PiTest的突变体丢弃。

3.2 实验设置

对于故障检测分析,我们使用了Defects4J v2.0,0,它包含了为Java程序重现(超过800个)真实故障的构建基础设施。数据集中的每个bug都由代码的错误和修复版本以及伴随项目的开发人员测试套件组成,其中包括至少 一个错误触发测试,该测试在错误版本中失败,在修复版本中通过。由于这是一个初步的评估,我们将目标锁 定数据集中bug数量较少的项目。准确地说,我们考虑了总共40个bug,报告了以下3个项目:Cli (22), Collections (2)和Csv(16)。对于断言评估分析,我们使用来自SpecFuzzer的数据集,这是Molina等人最近引入的一种规范推理技术,其中包括(41)个由开发人员手动编写的断言。每个主题包含源代码,在推理过程中使用的测试套件,以及手动编写的预期断言集。来自ground truth的6个断言被丢弃,因为它们不会杀死任何突变体。我们研究了µBERT是否可以帮助SpecFuzzer选择丢弃的断言,并与PiTest进行了比较。

RQ1:µBERT产生的突变体在检测实际故障方面的效果如何? 在故障检测方面,µBERT与PiTest相比如何?

方法:我们首先为每个故障的固定版本生成具有µBERT和PiTest的突变体。表一总结了这些工具生成的变异体数量。

然后,我们从生成的变异体数量和检测到的错误数量方面对这些技术进行了客观比较。从开发人员测试套件中选择最小的测试用例,为两个工具杀死相同数量的变异体,并检查它们是否检测到相关的真实故障。这一点很重要,因为µ ERT产生的突变比 PiTest少得多。然后,我们通过模拟测试人员选择突变体的场景来进行成本效益分析,他基于该场景设计测试来杀死它们。

我们首先采取由工具创建的变异体集合,随机拾取一个变异体并选择一个杀死它的测试或判断该变异体为等效并丢弃它。然后我们对集合中的所有变异体进行这个测试,丢弃被杀死的变异体。我们重复这个过程,直到达到杀死的变异体的最大数量。我们采用开发人员分析变异体的次数作为工作量/成本指标。这意味着,工作量是所选测试的数量加上判断为等效的变异体数量。然后我们检查生成的测试套件是否检测到真正的错误。我们重复这个过程100次,以减少随机选择变异体和杀死测试对我们结果的影响。这种具有成本效益的评估旨在强调不同突变体生成方法的影响。

表1

结果:图6总结了µBERT和PiTest的故障检测能力。

图6a显示, 杀死µBERT中所有突变的测试套件可以检测出40个故障中 的27个(67.5%)。而杀死所有PiTest突变体的套件可以检测到40个错误中的26个(65.0%)。有11个(27.5%)故障未被µB ERT和PiTest检测到。当我们检查重叠时,我们观察到µBERT检测到的3个故障是PiTest未检测到,2个PiTest检测到的故障未被µBERT检测到。 这表明µBERT的故障检测效率与PiTest相当,并且µBERT突变体可以潜在地补充其他突变检测技术。

图6b总结了这些技术的成本效益评估;故障检测有效性 (y轴)相对于相同数量的分析突变体(工作量)(x轴)。100%的努力意味着分析了最大可能的突变数(对于µBERT),在PiTest 的情况下,这与µBERT的数量相同,以便进行公平的比较。 如表1所示,PiTest比µBERT产生更多的突变体,因此杀死所有突变体需要比µBERT付出更多的努力。我们观察到µBERT更具成本效益,这表明当分析相同数量的突变体时,基于µ BERT突变体选择的套件比PiTest选择的套件更容易发现真正的故障。

图6c强调了这种具有成本效益的比较,并特别关注了分析最大突变体数量(即µBERT产生的突变体总数)时的故障检测率。平均而言,杀死µB ERT中所有突变的测试套件检测到真正故障的可能性为40.0% (46.0%);而杀死完全相同数量的PiTest突变体的套房为20.0%(39.5%)。

RQ2:断言评估分析

方法:我们首先使用µBERT和PiTest为所分析的四种方法生成突变体。然后我们运行推理工具,SpecFuzzer,为感兴趣的方法获得一组有效的断言(即从未被测试套件证伪)。然后,SpecFuzzer对推断的断言执行突变分析,并丢弃那些不能杀死任何突变的断言。我们确认了来自ground truth的6个断言在这个过程中被丢弃了。因此,我们再次运行SpecFuzzer的突变分析,但在这种情况下,我们考虑来自µBERT和PiTest的突变,并分析是否丢弃了基础真值断言。

结果:表2总结了SpecFuzzer在使用µB ERT和PiTest的突变体选择断言时的性能。对于每个工具,我们报告了基本事实中的断言是被选择还是被丢弃(Suc。列),我们还报告生成和杀死的突变体数量(分别为#M和#K)。我们可以观察到, 在分析的6个断言中,有3个杀死了µBERT产生的一些突变体,因此,SpecFuzzer没有丢弃它们。在PiTest的情况下,它有助于从基础事实中保留6个断言中的2个,但通常它比µBERT产生更多的突变(例如,在StackAr程序中高达10倍),这影响了过滤断言所需的时间。

表2

RQ3:µBERT突变体的定性分析

方法:为了回答RQ3,我们讨论了来自µBERT的一些例子,以及它可以为突变测试和断言推理方法提供的潜在好处。

结果:表III显示了µBERT产生的突变体的示例,这些突变体有助于找到PiTest未发现的三个实际故障(即id为Cli 10, Csv 15和Csv 16的故障)。对于每个案例,我们报告了fixed和 buggy之间的差异版本,以及固定版本与µBERT产生的突变体之间的差异。

表3

红色的线条对应固定版本,绿色的线条对应有bug的版本和变异体。

Cli 10表示的真正错误位于文件Parser.java中,位于函数setopoptions中,问题是它在内部对象字段requiredOp tions和作为参数给出的对象选项的相同字段之间创建了混叠。µBERT通过getter方法getRequiredOptions()生成与该字段交互的突变体。例如,MUTANT 1更改了一个if 条件,该条件与包含所需选项的列表的大小有关。MUTANT 2通过add更改方法调用remove,那么requiredO ptions列表将添加一个元素而不是删除它。

Csv 15是printAndQuote方法中的一个真正的错误,它位于类CSVFormat.java中,其中打印序列中的一些字符在解析器中导致失败。µBERT生成突变,改变预定义的特殊令牌,稍后用于打印字符串。例如,MUTANT 3 通过始终返回0而不是预设的分隔符标记来更改函数 getDelimiter()的返回值。当在初始化要打印的值的条件中调用toString时,MUTANT4将对象value替换为object this。

表示为Csv 16的错误存在于文件CVSParser.java中,精确地在类CSVRecordIterator中,该类实现了返回Csv记录的迭代器。µBERT产生突变,改变程序的控制流程,例如,突变的表达this。MUTANT 5中current == current的值总是为true,而MUTANT 6在函数isClosed中引入了无限递归。MUTANT 7修改addRecordValue方法中变量 inputClean的初始化,该方法稍后由迭代器使用。

表4显示了µBERT生成的突变体,这些突变体有助于SpecFuzzer不放弃从基础事实中获得的良好断言。特别是,为method Angle创建的3个变异体。getTurn显然违反了断言abs(res) <= 1,因此,它不会被丢弃。

表4

在复合的情况下。我们可以观察到MUTANT 4用c. update(this)代替了c.setParent(this)的调用。这个突变体使得子对象c (c.v evalue)的值被更新为对象This(父)的值。然后,断言c.value == old(c.value)将被这个突变体明显违反,因此不会被SpecFuzzer丢弃。

类似地,MUTANT 5用children.add(p)替换调用祖先. add(p)。这个突变体显然可以改变children的set值。断言children == old(children)显然会杀死这个突变体,因此 SpecFuzzer将保留它。

转述:韩廷旭

0 阅读:0

互联不一般哥

简介:感谢大家的关注