基于深度语义学习的测试补全

互联不一般哥 2024-04-08 09:56:41

Learning Deep Semantics for Test Completion

Pengyu Nie, Rahul Banerjee, Junyi Jessy Li, Raymond J. Mooney, Milos Gligoric

UT Austin, USA

引用

Nie P, Banerjee R, Li J J, et al. Learning Deep Semantics for Test Completion[J]. arXiv preprint arXiv:2302.10166, 2023.

论文:[2302.10166] Learning Deep Semantics for Test Completion (arxiv.org)

摘要

本文提出了TECO,一个利用代码语义进行测试补全的深度学习模型。传统的代码补全模型仅基于语法信息进行代码补全,而TECO提取了包含先前语句的执行结果和测试执行的上下文信息在内的六种语义信息,以此进行测试代码的补全。

1 引言

软件测试是工业中检查软件正确性的最常见的方法。然而,手动编写测试是无聊且耗时的。机器学习是应对该问题的一种思路,用手动编写的测试用例训练模型,并在后续编写新的测试时应用该模型。我们的目标是设计一种机器学习方法,以帮助开发人员在编写测试时提高生产力。因此,我们提出了一个新的任务——测试补全(Test Completion)。测试人员在编写测试方法的时候,可以随时利用测试补全来自动化的获取当前测试方法的下一句代码。

尽管测试补全与代码补全(Code Completion)密切相关,但二者又有所不同,因为测试代码有几个独特的特征。首先,测试中的其他方法为补全测试方法提供了额外的上下文。其次,测试代码有着与普通代码不同的编程风格,因为它与待测方法相关。具体来说,测试方法通常由以下顺序的语句序列组成:准备待测方法的输入——执行待测方法——使用断言语句(即测试预言)检查执行的结果。

我们提出了首个基于机器学习的解决方案——TECO,它使用代码语义信息作为ML模型的输入,并通过测试执行重新排序。其中,语义信息指的是在语法级数据(即源代码)中无法获取的与测试/代码执行相关的信息。TECO使用软件工程工具提取代码语义(例如,本地变量的类型),并将它们直接提供给模型。在生成了top-k个预测语句后,TECO将通过执行生成的语句来进一步确保输出质量,并使可运行和可编译的语句的优先级优于其他语句。

基于软件分析的经验,我们考虑了六种不同的软件分析的语义,总体来说可以分为两类:(1)执行结果,包括局部变量的类型和字段是否初始化;(2)执行上下文,包括设置和拆卸方法,测试方法中最后调用的方法,以及具有类似先前语句的非测试代码。

我们实现了支持JAVA测试的TECO,并在一个新收集的语料库上进行评估,该语料库包含130,934个测试方法,包含来自1,270个项目的645,633条语句。我们在这个语料库上对TECO进行了广泛的评估——包括词相似性、功能正确性和下游应用(即测试预言生成任务)——以显示将语义信息应用于深度学习的重要性。我们使用一系列的指标将生成的语句与手工编写的语句进行比较:精确匹配精度(Exact-match accuracy)、top-10精度、BLEU、CodeBLEU、编辑相似度(Edit Similarity)和ROUGE。结果表明,TECO在所有指标上均显著优于仅使用语法级数据的基线。我们还通过编译和运行生成的语句来衡量功能的正确性,结果表明,TECO在29%的情况下可以生成可运行的下一条语句,而表现最好的基线模型的对应比例只有18%。此外,我们还评估了TECO应用于测试预言生成任务的表现。TECO的精确匹配精度为16,显著优于之前最先进的方法的精确匹配精度9。

总的来说,本文的贡献如下:

任务:我们提出了一种新的任务,能够帮助开发人员更快的编写测试方法。想法: 我们提出在设计ML模型用于代码相关的任务时考虑代码语义和代码执行情况。模型:我们开发了TECO,这是第一个用大规模的代码语义数据训练来进行测试补全的transformer模型。此外,TECO还可以通过执行代码进行重新排序。代码语义的使用对于在测试方法中正确地建模执行过程至关重要的。语料库: 我们从1270个开源项目中创建了一个包含130,934种测试方法的大型语料库。我们相信这个语料库也将用于许多与测试相关的其他任务。评估:我们进行了广泛评估,结果表明,无论是在测试补全方面还是在测试预言生成方面,TECO在所有自动指标上均显著优于基线。我们还通过编译和运行生成的语句来评估生成的代码的功能正确性。

2 技术介绍

2.1 测试补全任务

测试补全任务的目标是,给定一个不完整的测试方法,在该测试方法中自动生成下一条语句。我们假设向测试完成系统提供以下输入:(1)测试代码,包括测试方法的关联方法以及项目中的其他非测试方法代码。(2)测试方法签名。(3)不完整测试方法中的先前语句(可以为零或多个语句)。

图1:测试补全任务示例

图1中给出了测试补全任务的示例。其中,黄框展示了作为输入的测试方法、方法签名、先前语句的信息,而绿框则展示了测试补全任务期望的输出,即下一条测试代码语句。

我们试图在测试方法的方法体中生成语句,因此测试方法的签名(包括注释和测试方法的名称)仅作为输入,它们不是测试完成任务的预测目标。在补全一个测试方法时,我们也不考虑来自同一项目的其他已经可用的测试方法的上下文,以防止模型通过从其他类似的测试方法中复制代码来作弊。我们定义的测试补全任务适用于当开发人员已经知道测试(因此知道测试方法和测试方法签名),并希望完成下一个语句在任何时候在编写测试方法的场景。

2.2 代码语义提取

TECO对于六种不同的代码语义设计了静态分析方法进行提取,图2展示了TECO的一般工作流程,包括两个阶段:1. 收集阶段:给定一个项目,TECO收集所有代码元素的集合(包括类、方法、字段)。2. 分析阶段:TECO从代码元素中提取每种代码语义。代码语义可以根据其内容分为两类:执行结果和执行上下文。执行结果包括:(S1)局部变量类型、(S2)缺失类型、(S3)未设置字段、(S4)设置拆卸、(S5)最后调用的方法和(S6)相似语句。

图2:TECO工作流程

S1:局部变量类型

局部变量类型数据是指测试方法中局部变量的类型。类型的提取是通过部分解析测试方法的字节码,而不考虑变量的值,这比读取包含已声明的局部变量类型的局部变量表更准确。例如:AbstractWComponent comp = new SimpleComponent()中,comp的类型将被解析为SimpleComponent,这比AbstractWComponent更加准确。局部变量类型的数据提供了下一条语句中可以使用什么类型的测试输入的信息。

S2:缺失类型

缺失类型指的是调用待测方法需要但测试中尚未定义或准备好的类型。缺失类型需要考虑待测方法的参数类型以及待测方法对应类的类型(在待测方法是非static方法的情况下)。当测试方法中存在已经被初始化的对应类型的变量时,就不会将其作为缺失类型考虑。缺失类型的数据可以帮助模型在下一句中准备测试需要的变量。

S3:未设置字段

未设置字段指的是测试类和待测类中未被初始化的字段。我们将初始化定义为在测试方法、初始化方法或由二者在四步以内间接调用的赋值语句,没有被赋值的语句将被定义为未设置字段。未设置字段的数据可以帮助模型在下一句中进行可能的赋值操作。

S4:设置拆卸

设置拆卸数据指的是测试类中的构造和析构方法的源代码。当测试框架执行测试时,在测试方法之前执行设置方法来设置环境(例如,连接到数据库),并且在测试方法之后执行拆卸方法来清理环境。通过提供此上下文信息,测试补全系统可以知道在测试方法中有什么可供使用的环境,并且还可以避免复制设置/拆卸方法中已经存在的语句。

S5:最后调用的方法

最后调用的方法数据是前面语句中最后调用的方法的源代码,如果还没有调用方法,该数据则为空。该数据提供了关于先前语句中的更多上下文。

S6:相似语句

类似的语句数据是项目的非测试代码中的一个语句,它与待补全的测试方法中的先前语句具有最相似的先前语句上下文。TECO使用BM25算法来搜索类似的先前语句。在搜索过程中只考虑了2个优先语句,因为增加窗口大小会导致更长的搜索时间,而没有提高返回的相似语句的质量。该信息可以辅助模型进行下一句代码的预测。

图3:语义信息示例

图3中给出了提取语义信息的示例,包含S2和S4两种。其中,S2缺失类型的信息是File,因为测试方法中尚未定义需要的File类型的变量。S4设置拆卸数据是GMOperationTest中的setup方法,它初始化了sut字段。

2.3 模型架构

图4展示了TECO深度学习模型的架构:一个基于编码器-解码器的transformer模型,它的输入包括输入是语法级的数据(测试中的方法、测试签名和先前的语句)和从要待补全测试中提取的代码语义信息(S1-S6),输出是下一个语句。

具体来说,TECO将待补全测试T和测试C下的代码作为输入,其中C包括测试下的方法,T由两部分组成:测试签名和先验语句,目标是生成下一个语句y。然后,TECO提取语义部分的信息:

然后,将语义信息与语法信息用BPE算法序列化,然后用分隔符<sep>进行连接,变为单一输入序列,若最后token数量大于最大限制512,则会对序列从头进行截断,因而将更重要的信息放在末尾。

图4:TECO模型架构

然后,TECO使用一个transformer模型学习条件概率分布p(y|x),该模型首先用编码器将输入序列编码为深度表示,然后用解码器依次解码每个token,最终输出结果序列。

由于预训练仅在语法级数据上进行而缺少语义信息,因此我们认为微调对于测试补全任务来说至关重要。在TECO的微调期间,模型学习除了理解语法级数据外,还学习如何理解和处理额外的代码语义。我们使用了预训练的CodeT5模型,并使用一个语料库TC,通过最小化交叉熵损失来对模型进行微调。

在评估时,TECO采用了束大小为10的束搜索(beam search)算法,从特殊的开始token<s>开始,TECO迭代运行解码器生成最可能的下一个token添加到输出序列,每一步只保留概率最高的前10个token,直到生成特殊的结束token</s>。我们还采用了惩罚机制,以减少产生重复结果的情况。

尽管模型可以通过最大化其在微调过程中学习到的生成概率来产生合理的输出,但是,我们无法保证生成的语句是可编译和可运行的代码。而在实际应用场景中,可执行的代码往往比合理但不可执行的代码更适合开发人员进行测试的编写。因此,我们采用了重排序(Reranking)的方式来提高生成语句的质量。

具体来说,在收集了束搜索按概率排序的前10个预测后,TECO会检查它们是否都可编译和可运行。然后,TECO将输出重新排序为,其中一个输出排名高于另一个,当且仅当:(1)可运行,不能;或(2)两者都不可运行,但是可编译的,不是;或(3)两者具有相同的可运行和可编译状态,但p()>()。通过这种方式,可编译和可运行的生成语句将优先于其他语句。

TECO通过将生成的语句置入一个特定测试类,来判断其是否可编译和运行。该测试类中有着需要的,但不会受到同一项目中的其他测试方法的影响。具体流程如图5所示:

1. 使用模型输入的方法签名和先前语句创建一个具有测试方法的测试类,并将生成的语句置于之后。

2. 将原始测试类中的其他非测试方法(包括设置、拆卸和工具方法)提取到已创建的类中。

3. 生成一个特定的main方法,按顺序调用设置方法,测试方法和拆卸方法。

4. 将生成的类、项目的构建配置中指定的所有依赖项和项目中的所有非测试类一起编译。

5. 如果编译成功(此时该语句被认为是可编译的),则执行已编译的类;只有在执行过程中没有异常或断言失败时,该语句才被认为是可运行的。

如果该项目使用了JUnit 4测试框架,则调用相关命令执行,如果没有,则用编写的特定main方法进行执行。

图5:可编译与可执行性判断流程

2.4 语料库

由于测试补全是一项新的任务,我们构建了一个大规模的语料库作为我们的工作和未来研究的测试平台。我们从CodeSearchNet使用的同一主题项目中收集数据。CodeSearchNet是一个大型的代码和评论语料库,在ML+代码研究中经常使用。在CodeSearchNet的4767个Java项目中,我们使用了1535个项目,条件如下: (1)使用Maven构建系统(仅仅是为了简化数据收集,TECO本身不局限于任何构建系统);(2)可以成功编译;(3)有允许使用的证书。

为了从这些项目中提取测试方法,我们首先使用相同的工具链收集每个项目的代码元素集,用于TECO静态分析的收集阶段。我们确定了用JUnit 4和JUnit 5测试框架编写的测试方法。具体来说,我们搜索了带有测试注解(@org.junit.Test或@org.junit.jupiter.api.Test)且没有忽略测试注解(@org.junit.Ignore或@org.junit.jupiter.api.Disabled)的方法。上述搜索得到了221,666种测试方法。然后,我们进一步过滤了测试方法,以保证语料库的质量。我们过滤了命名不规范(例如:test0)或不遵循测试用例签名惯例(例如:参数列表不为空,返回值不为void)的测试方法。接着我们使用了Waston等人提出的增强方法定位待测方法:

1. 选择只有单一调用的方法

2. 如果可以,通过从测试类的名称中删除“test”来找到测试中的类

3. 如果可以,选择在第一个断言语句之前调用的最后一个方法

4. 如果可以,选择最后调用的方法

我们以此移除了36818种无法找到待测方法的方法。

然后,我们通过行号查找对应的字节码指令,并对数据进行了以下约束:至少有一条语句但不超过20条;待测最多有200个token;待测方法和测试方法加起来最多有400个token;测试方法中的每个语句最多有100个token。

我们还删除了几个在分析过程中引入额外开销的情况:使用if语句、循环和try块的测试方法,因为它们包含非顺序控制流,不适合通过预测之前给出的下一个语句(22435个情况);以及使用lambda表达式的测试方法,因为它们阻碍了许多静态分析算法的工作。最后,我们用一个公共标记“STR”替换数据中的字符串文字,因为字符串为深度学习的解决方案带来了挑战。经过过滤,我们获得了一个包含1270个项目的语料库,包含130,934种测试方法和645,633条语句。

3 实验评估

3.1 实验设置

研究问题。本文中,我们将研究以下研究问题:

RQ1:TECO在测试补全任务上的表现如何?它与基线相比如何?

RQ2:在可执行的评估集上,TECO可以多频繁地预测可编译和可运行的下一条语句?

RQ3:TECO在测试预言生成上的表现如何?和之前的工作相比如何?

RQ4:通过执行进行重新排序在多大程度上帮助更加准确的预测下一条语句?

RQ5:每种类型的代码语义如何帮助更准确地预测下一个语句,以及不同类型的代码语义如何互补?

评估数据集。2.4中描述的自构建语料库,表1展示了使用该语料库的信息。

表1:语料库信息

为了研究模型在预测可编译和可运行的下一个语句方面的能力,我们在可运行子集上评估模型。该子集包含来自4,223个测试方法的25,074个可运行语句。为研究测试生成任务,我们在测试预言子集上进行了实验,测试预言子集是验证集的子集,其中要生成的语句是测试方法中的第一个断言语句,包含4,212个语句。为了在测试预言生成任务上计算可编译和可运行的度量,我们使用可运行测试预言子集进行实验评估,它是测试预言子集的子集,包含3540条标准语句。

对比方法。 我们将TECO模型与以下仅使用语法级数据的基线模型进行了比较,包括CodeT5,CodeGPT。在进行测试预言生成实验时,我们和只使用语法级数据的过往研究进行了对比,包括ATLAS和TOGA。对于所有的基线模型,我们使用了作者推荐的默认超参数和训练配置。我们在整个语料库的训练和验证集上训练CodeT5和CodeGPT。我们在我们的训练和验证集的一个子集上训练ATLAS和TOGA,该子集只包含每个测试方法中的第一个断言语句,其中分别包含92,567个语句和3,050个语句。

评估指标。 包括词级别的指标:精确匹配精度(Exact-match Accuracy,XM),top-10精确度(Top-10 accuracy),BLEU,CodeBLEU,编辑相似性(Edit Similarity),ROUGE以及功能级别的指标:%Compile(可编译率),%Run(可执行率)

RQ1 补全效果比较

表2显示了TECO和基线模型进行测试补全任务的结果。我们的模型TECO在所有自动度量指标上都显著优于所有基线模型。TECO达到了17.61的精确匹配精度,比最佳基线模型CodeT5的13.57高出29%。这表明,使用代码语义和按执行重新排序可以大大提高深度学习模型在测试完成时的性能。

表2 补全任务结果

非微调的基线模型CodeT5-noFt无法解决测试完成任务。这是因为该模型没有测试补全任务的输入-输出格式的领域知识。

先前的研究表明,CodeGPT对生成类似上下文的代码的代码补全的任务是有效的。但是,在进行测试补全任务时,它的性能比基线CodeT5略差,因为该任务需要在测试方法中生成语句,该语句的风格与在提供的上下文中被测试的方法不同。

RQ2 功能正确性比较

表3展示了在可运行子集上的TECO和基线模型的运行结果,我们使用%Compile和%Run来衡量生成的语句的功能正确性。结果表明,TECO可以在28.63%的时间内生成可运行的语句,在76.22%的时间内生成可编译的语句,远远高于最佳基线模型的17.62%和54.84%。在使用的可运行子集上,TECO在其他词相似度的度量指标上也优于所有基线模型。

表3 可执行性实验结果

另外两个基线模型,CodeT5-noFt和CodeGPT,都无法生成任何可编译或可运行的语句。经过更仔细的检查,我们发现CodeT5-noFt无法处理输入,因而总是生成会中断的代码;而CodeGPT总是生成在Java中不是有效语句的代码,例如,以方法签名开头的代码。

RQ3 测试预言生成性能比较

表4和表5分别展示了TECO和基线方法在测试预言子集和可运行测试预言子集上进行测试预言生成任务的结果。结果表明TECO显著提高了这项任务的精确匹配精度(82%),从之前最先进的模型TOGA的9.01提高到16.44。TOGA的精确匹配精度与经过微调的CodeT5模型相当,这证实了TECO的改进主要来自于使用代码语义和通过执行重新排序。

在精确匹配精度方面,TOGA是该任务中最强的先前模型。然而,它在考虑部分匹配的其他评估指标上比基线CodeT5更差。这是因为TOGA是一个分类模型,它对使用启发式生成的一组候选断言语句进行排序,当标准语句不在其中时,该模型无法正确地对次优候选语句进行排序。

表4 测试预言子集实验结果

表5 可运行测试预言子集实验结果

RQ4 重排的优化效果

表6—表9展示了未经重排的TECO进行测试补全任务的实验结果。在评估集上,重排提高了2个百分点的精确匹配准确度。然而,在其他考虑部分匹配的评估指标上,重排的提升相对较小。这表明,重排在大多数情况下能够有效的将可执行的生成语句挑选出来,但在少部分情况下,它可能会将相比于原top-1语句而言,与标准语句相似度较小的可执行生成语句挑选出来。未重排的TECO-noRr模型的所有指标仍然显著优于CodeT5。

表6 验证集上的重排结果

在可执行子集上的结果表明,TECO相比TECO-noRr,显著的提高了%Compile和%Run指标,这表明重排是提高生成语句质量的有效策略。

表7 可执行子集上的重排结果

执行重排操作对于提高测试预言生成任务的性能非常重要,由表8和表9的结果可以看出,TECO在精确匹配精度上比TECOnoRr高出6-8个百分点,在%Run上高12个百分点。这是因为执行所生成的语句可以很容易发现断言语句中的逻辑错误(例如生成错误的期望值将导致断言失败)。

表8 测试预言子集上的重排结果

表9 可执行测试预言子集上的重排结果

RQ5 代码语义的比较

表10和表11分别展示了TECO只使用单一语义信息在验证集和测试预言子集上进行测试补全的结果,并与最好的基线CodeT5进行了对比。结果表明,每种语义信息都至少在一个度量上优于CodeT5,这意味着每个代码语义都提供了一些对测试完成有用的信息。

在表10中,TECO-S2(缺失类型)是BLEU、CodeBLEU、EditSim和ROUGE指标表现最好的模型,TECO-S4(设置拆卸)是精确匹配精度和top-10精度表现最好的模型。而在表11中,最好的模型出现了变化,TECO-S3(未设置字段)是BLEU、EditSim和ROUGE指标表现最好的模型,而TECO-S6(类似的声明)是精确匹配精度、top-10精度和CodeBLEU表现最好的模型。因此,不同类型的代码语义为测试完成提供了互补的信息。

表10 验证集比较结果

表11 测试预言子集比较结果

转述:高晨宇

0 阅读:0

互联不一般哥

简介:感谢大家的关注