论文部分内容阅读
软件项目中的代码普遍存在缺陷,这些缺陷可能会造成巨大的经济损失,甚至危及人类生命。软件开发人员一般通过编写测试用例,即单元测试,的方式来确保产品程序按预期的需求运行。尽管一般来说,测试代码被认为是没有缺陷的,但实际上并非如此。例如,当产品代码被更改时,若测试开发者未能及时更新测试代码,就会引发错误。在这种情况下,我们称这些测试代码是。直观地讲,测试代码和被测试的产品代码应该共同演化(Co-Evolution),即两者应该同时被修改、更新,否则,旧的测试代码可能会在新的产品代码逻辑中运行失败,造成开发人员的困扰和计算资源、时间等的浪费。先前的研究[21]指出,共同演化是耗费时间和资源的。也因此,在实际项目过程中,开发者很难维护产品和测试代码的共同演化。文献[8]指出,与编写产品代码相比,开发人员通常需要花费更多的时间阅读并理解测试用例的代码,而他们往往倾向于高估自己在维护测试代码时所花的时间。总的来说,尽管维护测试用例是很重要的工作,但在实际软件项目中经常没有得到重视。现有研究较少关注过时的测试用例的问题。已有的关于“产品-测试共同演化”的研究工作主要关注的是产品代码和测试代码的关联规则(Association Rule)以及可追溯性(Traceability)。这类工作的主要通过静态分析或动态执行的方式,寻找与测试用例代码和与之关联的产品代码,以及在此之上挖掘更加一般的规则,例如共同演化的类型、粒度、项目间的模式,等。典型的这一类工作例如[31][33][51][52]。也有的工作关注共同演化的可视化,通过这种可视化能够发现共同演化在时间、软件过程阶段中的分布,如[58]。这些工作只关注在已有的软件仓库中挖掘模式,但没有考虑如何利用这些模式做自动化的测试代码调试、演化。自动化的软件工程通常有两技术:缺陷定位(Fault Localization,FL)和自动程序修复(Automated Program Repair,APR)。然而,应用这些技术在测试代码演化上有两个难点。对于缺陷定位,特别是对于基于频谱的技术,需要运行多个测试用例来发现可能出错的程序位置,该位置通常会被更多的错误用例执行过。由于每个测试用例(或测试方法)仅执行一次,且执行路径通常不包含分支,因此这种技术对定位测试代码中的缺陷没有效果。对于自动程序修复而言,主要难点在于难以生成程序补丁。先前的工作主要研究如何修复产品源代码,其中一些甚至只关注修复条件语句,但测试代码中通常很少有条件语句,也因此不能修复过时的测试代码。为了填补现有的关于“产品-测试代码共同演化”研究工作的缺失,本文提出了一种基于机器学习的预测方法,用于在产品代码发生更新时,判断其所对应的测试代码是否需要更新。该方法参考了在软件工程被大量研究的软件缺陷预测(Software Defect Prediction)方法,不同之处在于,该方法关注的是产品软件的更新造成的测试代码缺陷,与现有的工作相比,能够处理测试代码的缺陷预测问题,具有一定的创新之处。机器学习的框架中需要考虑样本特征、标签、分类器等问题,而这些问题我们通过实证研究来加强其合理性。实证研究的目的是评估真实软件项目中产品和测试代码共同演化的场景。为此,我们从最大的开源基金会,Apache软件基金会,下载了975个开源Java项目,从中挖掘产品测试共同演化的配对,并探究它们在演化类型、演化时间,以及演化目上的特点。研究发现,大多数共同演化都是产品代码和测试代码同时被编辑,并且这种演化方式占有绝大部分的比例。同时,开发者也倾向于对产品代码和测试代码应用相同的更改,例如同时新增、删除一对“产品-测试”的组合。此外,这些项目也存在少数比较反直觉的共同演化组合。例如,当软件开始大规模重构时,某个产品代码会被删除,但同时开发者会创建其对应的测试代码。在时间分布上,大部分的共同演化都是同时发生的,即,产品更改和测试更改都在同一个提交中。当测试代码的更新晚于产品代码时,它们之间的时间间隔一般不会相隔很久,通常都在48小时以内。这些共同演化在所有的代码历史更改中只占了较小的一部分(低于20%),因此可以说,共同演化在软件项目中不是普遍存在的。为了探究这种共同演化的目的,我们又随机抽取了157个共同演化的产品更改,这个样本数量在我们所有的例子中能达到95%的置信度,因此具有显著的统计学意义。这157个样本被人工划分为6种不同的共同演化目的,其中最突出的两种目的是,产品代码的行为发生了变化,或者进行了重构。这两种目的在所有的共同演化目的中占了75%以上的比例。其他目的包括代码相关的修改,如数据成员变化、代码注解;也包括非代码的修改,例如版权信息更新、注释修改。基于实证研究的发现,我们设计了用于预测测试代码更改状态的方法。该方法涉及了基于静态代码分析的特征工程,以及基于分类学习的更改状态预测。为了提取语法结构生成特征,本文在普通模拟算法之外,又提出了一种基于区间穿刺问题(Interval Stabbing Problem)的特征抽取算法,经分析可知该算法比起普通模拟算法,在输入规模大时具有更优的时间复杂度。该方法一共提取了14个基于语法结构的代码变化特征,可以用于表示多粒度的代码行为的变化。同时,为了提升分类算法的准确度和泛化性,将纯粹注释相关的样本都排除在数据集外,因为从这些样本中抽取的特征都是0,无法作为训练数据;而纯粹注释修改造成的共同演化,显然是没有参考价值的。对于样本标签的判断,我们考虑了正负两种样本,用以表示代码在48小时内产生了共同演化、或者10天内没有出现共同演化。该阈值的设定也与实证研究中的发现有关。对于正样本,根据其具体的更新类型,又可以细分为添加、更改、删除三种文件修改类型。这套方法是可扩展的、端到端的方法,且适用于多种不同的机器学习算法,能够在大规模的数据集中被验证和应用。为了评估该预测方法流程,我们在不同的样本配置和模型参数下进行了多项实验,验证了该方案的可行性。我们使用了二分类、四分类、类别平衡学习和类别不平衡学习,等多种样本配置,发现对于二分类和类别平衡学习来说,该方案有较好的泛化性。实验结果表明,对于二分类问题,应用随机森林,该方法能够达到平均71.44%的精确率和68.43%的召回率,而多分类的表现结果略有下降。因此,该方法能够有效地预测测试代码的更新状态。通过分析不同特征的贡献,我们发现方法上的代码更改对于开发者决策测试用例是否需要更新具有较为显著的作用。例如,使用单个的“新增方法行数”特征,即可以让模型达到60%以上的准确率;但如果用“删除属性行数”来学习,准确率只有52%,说明这是最不重要的决策因素。最后,通过总结工作,提出了后续研究的计划,如,提升模型表现(即精确率、准确率,等),以及通过挖掘更细粒度的代码共同演化的更改,指导自动更新测试用例,从而帮助开发者更好地维护软件项目中的测试代码。