没有时间进行完整重构时,为遗留代码编写测试是否有意义?


72

我通常会尝试遵循《与Legacy Cod e 一起有效工作》一书的建议。我打破了依赖关系,将部分代码移到@VisibleForTesting public static方法和新类上,以使代码(或至少部分代码)可测试。并且我编写测试以确保在修改或添加新功能时我不会破坏任何东西。

一位同事说我不应该这样做。他的推理:

  • 最初,原始代码可能无法正常工作。并且为它编写测试使将来的修复和修改变得更加困难,因为开发人员也必须理解和修改测试。
  • 如果它是具有某些逻辑的GUI代码(例如〜12行,例如2-3 if / else块),则由于代码太琐碎而无法开始,因此测试不值得这样做。
  • 类似的不良模式也可能存在于代码库的其他部分中(我还没有看到,我还很新)。一次大的重构就更容易清理它们了。提取逻辑可能会破坏这种未来的可能性。

如果我们没有时间进行完整的重构,是否应该避免提取可测试的部分并编写测试?我应该考虑这样做的不利之处吗?


29
您的同事似乎只是在找借口,因为他没有那样做。人们有时会这样表现,因为他们太顽强而无法改变他们习惯的做事方式。
布朗

3
代码的其他部分可能会将应归为错误的内容转变为功能
棘手怪胎2014年

1
我能想到的唯一反对这一观点的好观点是,如果您误读/混淆了某些内容,那么重构本身可能会引入新的错误。因此,我可以在当前正在开发的版本上自由地重构和修正我的内心内容,但是对以前版本的任何修复都面临着更高的障碍,并且如果它们仅仅是“整容”的,则可能不被批准,因为该风险被视为超过潜在收益。了解您的当地文化-不仅是牛郎的想法-并且在做其他任何事情之前都要有充分的充分理由。
keshlam 2014年

6
第一点有点好笑–“不要测试,它可能是越野车。”好吧,是吗?那么,很高兴知道这一点–我们是要解决这个问题,还是我们不想让任何人将实际行为更改为某些设计规范所说的。无论哪种方式,测试(以及在自动化系统中运行测试)都是有益的。
Christopher Creutzig 2014年

3
由那些只想将他们认为无聊的事情(编写测试)推向遥远的未来的人们炮制的事实是,即将发生的“大重构”能够治愈所有疾病是一个神话。如果它真的成为现实,他们将让它变得如此之大而感到遗憾!
朱莉娅·海沃德

Answers:


100

这是我个人的不科学印象:所有三个原因听起来像是普遍的但错误的认知错觉。

  1. 当然,现有代码可能是错误的。也可能是正确的。由于整个应用程序似乎对您有价值(否则您将直接丢弃它),因此在没有更多具体信息的情况下,您应假定它主要是正确的。“编写测试会使事情变得更加困难,因为总体上涉及的代码更多”,这是一种过于简单的态度,而且非常错误。
  2. 务必在您用最少的努力获得最大价值的地方进行重构,测试和改进工作。值格式的GUI子例程通常不是第一要务。但是因为“很简单”而不进行测试也是一种非常错误的态度。实际上,所有严重错误都是在犯错的,因为人们认为他们比实际了解的要多。
  3. “将来我们将一口气完成所有任务”是一个不错的想法。通常,在未来,大势所趋会牢牢保持,而在目前,什么也不会发生。我坚信“缓慢而稳定地赢得比赛”的信念。

23
+1代表“几乎所有严重错误都是在发生,因为人们认为他们比实际了解的要多。”
rem 2014年

关于第1点-使用BDD进行测试是自我记录...
Robbie Dee 2014年

2
正如@ guillaume31所指出的那样,编写测试的部分价值在于证明代码的实际工作方式-可能符合或不符合规范。但这可能是规范的“错误”:业务需求可能已更改,并且代码反映了新的要求,但规范没有。简单地假设代码是“错误的”就太简单了(请参阅第1点)。再次,测试将告诉您代码实际执行的操作,而不是别人认为/说出的内容(请参见第2点)。
大卫

即使您一口气,也需要了解代码。即使您不重构而是重写,测试也将帮助您捕获意外行为(并且,即使重构,它们也有助于确保重构不会破坏传统行为-或仅破坏您希望破坏的行为)。随意合并或不合并-如您所愿。
Frank Hopkins

50

一些想法:

重构遗留代码时,编写的某些测试是否与理想规范相冲突并不重要。重要的是他们测试了程序的当前行为重构是关于采取微小的iso-functional步骤来使代码更整洁。您不想在重构时进行错误修复。此外,如果您发现了明显的错误,也不会丢失。您始终可以为其编写回归测试并暂时将其禁用,或者在积压中插入错误修正任务以供以后使用。一心一意。

我同意纯GUI代码很难测试,也许不太适合“ 有效工作... ”式的重构。但是,这并不意味着您不应该提取与GUI层无关的行为并测试提取的代码。而“ 12行,2-3 if / else块”并不是不重要的。所有带有至少一些条件逻辑的代码都应进行测试。

以我的经验,大型重构并不容易,而且很少起作用。如果您没有为自己设定精确的,微小的目标,则极有可能面临永无休止,令人毛骨悚然的返工,而您最终将永远无法站起来。更改越大,您越有可能破坏某些东西,而您发现失败原因的麻烦也就越大。

通过临时的小型重构使事情逐步变得更好,但这并不是“破坏未来的可能性”,而是使它们成为可能-巩固了应用程序所在的沼泽地。您绝对应该这样做。


5
+1表示“您编写的测试可以测试程序的当前行为
David

17

另外请注意:“原始代码可能无法正常工作”-这并不意味着您无需担心影响即可更改代码的行为。其他代码可能依赖于似乎破坏行为的行为或当前实现的副作用。现有应用程序的测试覆盖范围应该使以后的重构更加容易,因为它可以帮助您发现意外损坏的内容。您应该首先测试最重要的部分。


可悲的是。我们有一些明显的错误,这些错误会在无法解决的极端情况下显现出来,因为我们的客户更喜欢一致性而不是正确性。(它们是由于数据收集代码引起的,该数据收集代码允许不考虑报告代码的事情,例如将一系列字段中的一个字段留空)
Izkata 2014年

14

Kilian的答案涵盖了最重要的方面,但我想扩展一点1和3。

如果开发人员想要更改(重构,扩展,调试)代码,则必须理解它。她必须确保所做的更改完全影响她想要的行为(在重构的情况下没有影响),并且没有其他影响。

如果有测试,那么她也必须理解测试。同时,测试应该可以帮助她理解主代码,而且无论如何,测试比功能代码更容易理解(除非它们是不好的测试)。这些测试有助于显示旧代码的行为发生了什么变化。即使原始代码错误,并且测试针对该错误行为进行了测试,这仍然是一个优势。

但是,这要求将测试记录为测试先前存在的行为,而不是规范。

关于第三点也有一些想法:除了实际上很少发生“大举动”这一事实外,还有另一件事:实际上并不容易。为了简单起见,必须满足几个条件:

  • 需要重构的反模式很容易找到。您所有的单身人士都命名XYZSingleton吗?他们的实例获取器总是被调用getInstance()吗?以及如何找到您的过深层次结构?您如何搜索您的神物?这些需要对代码指标进行分析,然后手动检查指标。或者您像工作时那样偶然发现它们。
  • 重构需要机械的。在大多数情况下,重构的难点是对现有代码有足够的了解,以了解如何对其进行更改。再次单身人士:如果单身人士不见了,您如何向其用户获取所需的信息?它通常意味着了解本地书法,以便您知道从何处获取信息。现在,更容易的是:在应用程序中搜索十个单例,理解每个单例的用法(这导致需要了解60%的代码库),然后将它们剔除?还是采用您已经了解的代码(因为您现在正在处理它)并从中删除正在使用的单例?如果重构不是那么机械,以至于几乎不需要或几乎不需要了解周围的代码,那么就没有用。
  • 重构需要自动化。这有点基于观点,但是这里有。一点重构很有趣并且令人满足。许多重构乏味而乏味。在继续进行更有趣的工作之前,将刚刚处理的代码片段置于更好​​的状态会给您一种愉悦,温暖的感觉。试图重构整个代码库会使您感到沮丧,并对编写它的白痴程序员感到愤怒。如果您想进行大幅度的重构,则需要在很大程度上实现自动化,以最大程度地减少挫败感。从某种意义上讲,这是前两点的融合:只有能够自动发现错误代码(即容易找到)并自动更改(例如机械性)代码,才能使重构自动化。
  • 逐步改进可带来更好的业务案例。大幅度的重构令人难以置信。如果重构一段代码,您总是会与其他从事此工作的人发生合并冲突,因为您只是将他们要更改的方法分为五个部分。当重构一段合理大小的代码时,您会与几个人发生冲突(拆分600行宏功能时发生1-2,拆分上帝对象时发生2-4,拆分模块中的单例时发生5 ),但由于您的主要修改,您还是会遇到这些冲突。在进行代码库范围的重构时,您会与所有人发生冲突。更不用说它将几天的开发人员联系在一起。逐步改进会使每次代码修改花费更长的时间。这使得它更具可预测性,并且没有一个可见的时间段,除了清除以外,什么都没有发生。

12

一些公司有一种文化,他们乐于允许开发人员随时增强不能直接提供附加价值的代码,例如新功能。

我可能在这里宣教the依者,但这显然是虚假的。简洁简洁的代码使后续开发人员受益。只是回报并不立即显现。

我个人订阅《童子军原则》,但其他人(如您所见)不同意。

就是说,软件遭受熵的困扰,并增加了技术负担。之前时间不多的开发人员(或者可能只是懒惰或缺乏经验的开发人员)可能在设计良好的解决方案上实施了次优的越野车解决方案。尽管重构它们似乎是可取的,但是您冒着将新的错误引入(无论如何对用户而言)工作代码的风险。

某些更改比其他更改具有较低的风险。例如,在我工作的地方,往往有很多重复的代码可以安全地移植到子例程中,而影响最小。

最终,您必须对重构进行多远做出判断,但是,如果自动化测试尚不存在,那么毫无疑问,添加自动化测试将具有价值。


2
我完全同意原则,但是在许多公司中,这取决于时间和金钱。如果“整理”部分只需几分钟,那很好,但是一旦整理的估算值开始变大(对于“大”的定义),您需要编码人员将该决定委托给您的老板或专案经理。这不是您决定花费时间的价值的地方。使用错误修复X或新功能Y可能对项目/公司/客户具有更高的价值。
ozz 2014年

2
您可能还没有意识到更大的问题,例如该项目在6个月的时间内被废弃,或者仅仅是公司更珍惜您的时间(例如,您做他们认为更重要的事情,而其他人则要做重新设计工作)。重构工作也会对测试产生影响。大型重构会触发完整的测试回归吗?公司是否有可以部署的资源来做到这一点?
ozz 2014年

是的,正如您所谈到的那样,进行大型代码手术可能不是好主意有很多原因:其他开发优先级,软件的生命周期,测试资源,开发人员经验,耦合,发布周期,对代码的熟悉程度基地,文档,任务关键性,公司文化等等等。这是一个判断电话
Robbie Dee 2014年

4

以我的经验,某种形式的表征测试效果很好。它可以相对较快地为您提供广泛但不是很具体的测试范围,但是要为GUI应用程序实施可能会比较棘手。

然后,我将为要更改的零件编写单元测试,并且每次都要进行更改时都要这样做,从而随着时间的推移增加单元测试的覆盖范围。

如果更改影响到系统的其他部分,则此方法为您提供了一个好主意,让您可以尽早进行所需的更改。


3

回复:“原始代码可能无法正常工作”:

测试不是一成不变的。它们可以更改。而且,如果您测试了错误的功能,则应该容易地更正确地重写测试。毕竟,仅应更改已测试功能的预期结果。


1
IMO,单个测试应该写成石头,至少直到测试的功能失效。它们可以验证现有系统的行为,并帮助确保维护者其更改不会破坏可能已经依赖该行为的旧代码。更改实时功能的测试,您将失去这些保证。
cHao 2014年

3

嗯,是。回答为软件测试工程师。首先,您应该测试您无论如何所做的所有事情。因为如果您不这样做,您将不知道它是否有效。对于我们来说,这似乎很明显,但是我的同事对此有不同的看法。即使您的项目只是一个可能永远无法交付的项目,您也必须直面用户并说您知道它可行,因为您已经对其进行了测试。

非平凡的代码始终包含错误(引用uni中的一个人;如果其中不包含错误,则是不重要的),我们的工作是在客户之前找到它们。旧版代码具有遗留错误。如果原始代码无法正常运行,您想了解一下,请相信我。如果您知道错误,就可以了,不要害怕找到它们,这就是发行说明的目的。

如果我没记错的话,《重构》一书说无论如何都要不断进行测试,所以这是过程的一部分。


3

进行自动测试。

当心自己和客户以及老板的一厢情愿。我很想相信我的更改将是第一次正确,并且只需要测试一次,就像学会对待这种想法一样,我对待尼日利亚的欺诈邮件也是如此。好吧,主要是;我从来没有收到过诈骗电子邮件,但是最近(当我大喊大叫时)我放弃了不使用最佳做法。这是一次痛苦的经历,一直拖延(昂贵)。再也不!

我从Freefall网络漫画中得到一条最喜欢的名言:“您曾经在一个复杂的领域工作过,而主管对技术细节只有一个粗略的了解吗?...那么,您知道导致主管失败的最可靠方法是:毫无疑问地遵循他的每一个命令。”

限制投资时间可能是合适的。


1

如果您要处理的大量遗留代码目前未在测试中,那么现在就进行测试,而不是等待将来的假设性大笔重写是正确的做法。不是通过编写单元测试开始的。

如果没有自动测试,则在对代码进行任何更改之后,您需要对应用程序进行一些手动的端到端测试,以确保其正常运行。首先编写高级集成测试来代替它。如果您的应用程序读取文件,对其进行验证,以某种方式处理数据,并显示您想要捕获所有内容的测试结果。

理想情况下,您将获得来自手动测试计划的数据,或者能够获取要使用的实际生产数据的样本。如果不是这样,则由于该应用程序已经投入生产,在大多数情况下,它正在按照应有的方式工作,因此只需弥补将达到所有高点的数据,并假设目前的输出是正确的即可。这并不比承担一个小的功能,假设它正在执行其名称或任何注释表明应执行的功能以及编写测试(假设其正常工作)更糟糕。

IntegrationTestCase1()
{
    var input = ReadDataFile("path\to\test\data\case1in.ext");
    bool validInput = ValidateData(input);
    Assert.IsTrue(validInput);

    var processedData = ProcessData(input);
    Assert.AreEqual(0, processedData.Errors.Count);

    bool writeError = WriteFile(processedData, "temp\file.ext");
    Assert.IsFalse(writeError);

    bool filesAreEqual = CompareFiles("temp\file.ext", "path\to\test\data\case1out.ext");
    Assert.IsTrue(filesAreEqual);
}

一旦编写了足够的这些高级测试来捕获应用程序的正常运行和最常见的错误情况,您将需要花费大量时间在键盘上敲打,以尝试从代码中捕获错误,而不是执行其他操作您以为应该做的事情会大大减少,从而使将来的重构(甚至大笔重写)变得更加容易。

由于您能够扩展单元测试的覆盖范围,因此您可以缩减甚至淘汰大多数集成测试。如果您的应用程序正在读取/写入文件或访问数据库,则隔离测试这些部分,然后模拟它们或通过创建从文件/数据库读取的数据结构开始进行测试是一个显而易见的起点。实际上,创建该测试基础结构将比编写一组快速而肮脏的测试花费更多的时间。并且每次您运行2分钟的集成测试集,而不是花30分钟手动测试集成测试所涵盖内容的一小部分,您就已经取得了巨大的成功。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.