为什么要为将要重构的代码编写测试?


15

我正在重构一个巨大的遗留代码类。重构(我认为)主张:

  1. 为遗留类编写测试
  2. 摆脱困境

问题:一旦我重构了类,就需要更改步骤1中的测试。例如,以前是旧方法中的东西,现在可能是一个单独的类。一种方法现在可能是几种方法。遗留类的整个景观可能被抹杀为新的东西,因此我在步骤1中编写的测试几乎是无效的。本质上,我将添加步骤3。大量重写我的测试

那么重构之前编写测试的目的是什么?这听起来更像是为自己创造更多工作的学术活动。我现在正在为该方法编写测试,并且正在学习有关如何测试事物以及旧方法如何工作的更多信息。可以通过阅读旧代码本身来学习这一点,但是编写测试几乎就像在摸摸我的鼻子,并且在单独的测试中记录这种临时知识。因此,以这种方式,我几乎别无选择,只能学习代码在做什么。我在这里说的是暂时的,因为我会从代码中重构出乱七八糟的东西,并且我的所有文档和测试在很大程度上都是无效的,除非我的知识会留下来并使我对重构更加新鲜。

这是重构之前编写测试的真正原因-帮助我更好地理解代码吗?还有另一个原因!

请解释!

注意:

一篇文章:没有时间进行完全重构时,为遗留代码编写测试是否有意义?但是它说“重构之前先写测试”,但没有说“为什么”,或者说“写测试”看起来像“很快就会被销毁的繁忙工作”该怎么办。


1
您的前提不正确。您将不会更改测试。您将编写新的测试。步骤3将是“删除现在已终止的所有测试”。
pdr

1
然后,第3步可以阅读“编写新测试。删除失效的测试”。我认为这仍然等同于摧毁原始作品
丹尼斯

3
不,您想在步骤2中编写新的测试。是的,步骤1被销毁了。但这是在浪费时间吗?不,因为它使您放心,您在第2步中没有破坏任何东西。您的新测试没有。
pdr

3
@Dennis-尽管我对情况有很多相同的担忧,但我们可以将大多数重构工作视为“破坏原始工作”,但如果我们从不销毁它,那么我们永远也不会放弃将1万行写成意大利面条的代码文件。单元测试可能也应该如此,它们与所测试的代码齐头并进。随着代码的发展以及事物的移动和/或删除,单元测试也应随之发展。
DXM

“了解代码”是不小的优势。您希望如何重构您不了解的程序?这是不可避免的,还有什么比编写全面的测试更好的方法来证明对程序的真正理解。还应该说,测试越抽象,您以后再刮擦它的可能性就越小,因此,如果有的话,请首先坚持进行高级测试。
尼尔2014年

Answers:


46

重构就是在不更改(外部可见)行为的情况下清理一段代码(例如,改进样式,设计或算法)。你写测试不确保前和重构后的代码一样的,而不是你写测试作为一个指标,之前和之后重构你的应用程序的行为是一样的:新的代码兼容,并且没有引入新的错误。

您主要关心的是为软件的公共接口编写单元测试。该界面不应更改,因此测试(对此界面的自动检查)也不应更改。

但是,测试对于定位错误也很有用,因此也可以为软件的私有部分编写测试。这些测试预计将在整个重构过程中发生变化。如果要更改实现细节(例如私有函数的命名),则首先更新测试以反映更改后的期望,然后确保测试失败(您的期望未得到满足),然后更改实际代码并检查所有测试是否再次通过。公用接口的测试决不应开始失败。

当进行较大的更改(例如重新设计多个相互依赖的零件)时,这将更加困难。但是会有某种界限,在那个界限上,您将能够编写测试。


6
+1。读我的思想,写我的答案。重要提示:您可能需要编写单元测试,以表明重构后仍然存在相同的错误!
david.pfx

问题:为什么在您的函数名称更改示例中,您首先更改测试以确保测试失败?我想说的是,当您更改它时,它当然会失败-您断开了链接程序用来将代码绑定在一起的连接!您是否可能希望使用刚刚选择的名称来存在另一个现有的私有功能,并且必须验证情况是否如此,以防万一您错过了它?我认为这将为您提供一定程度的OCD保证,但是在这种情况下,这感觉有些过头了。您的示例中的测试不会失败吗?
丹尼斯

^ cont:作为一种通用技术,我认为最好对代码进行逐步的健全性检查,以尽早发现出错的地方。有点像您每次都不洗手可能不会生病,但是简单地洗手作为一种习惯可以使您的整体健康,无论您是否接触到被污染的物体。在这里,您有时可能会不必要地洗手,或者有时会不必要地测试代码,但这有助于保持您和代码的健康。那是你的意思吗?
丹尼斯

@Dennis实际上,我在不知不觉中描述了一项科学上正确的实验:当改变一个参数时,我们无法确定哪个参数实际上影响了结果。请记住,测试是代码,每个代码都包含错误。您会因为接触代码之前未运行测试而陷入程序员的地狱吗?当然不是:运行测试是理想的,但这是否必要是您的专业判断。还要注意,如果测试无法编译,则测试将失败,我的回答也适用于动态语言,而不仅适用于具有链接器的静态语言。
阿蒙2014年

2
通过在重构时修复各种错误,我意识到,如果没有测试,我将不会轻易完成代码移动。测试使我警觉了通过更改代码而引入的行为/功能“差异”。
丹尼斯

7

嗯,维护旧系统。

理想情况下,您的测试仅通过类与其他代码库,其他系统和/或用户界面的接口来处理该类。接口。您不能在不影响那些上游或下游组件的情况下重构接口。如果全部都是紧密耦合的混乱,那么您最好将其视为重写而不是重构,但这主要是语义上的。

编辑: 假设您的代码的一部分可以测量某些东西,并且它具有仅返回值的函数。唯一的接口是调用函数/方法/ Whatnot并接收返回的值。这是松耦合,易于单元测试。如果您的主程序有一个管理缓冲区的子组件,并且对它的所有调用都取决于缓冲区本身,一些控制变量,并且它通过另一部分代码消除了错误消息,那么您可以说这是紧密耦合的,并且很难进行单元测试。您仍然可以使用足够数量的模拟对象来执行此操作,但是会变得混乱。特别是在c。重构缓冲区如何工作的任何数量都会破坏子组件。
结束编辑

如果您通过保持稳定的接口来测试您的类,那么您的测试在重构前后应该是有效的。这使您可以自信地进行更改,而不会破坏它。至少,更有信心。

它还允许您进行增量更改。如果这是一个很大的项目,我认为您不会想要将其全部拆掉,构建一个全新的系统,然后开始开发测试。您可以更改它的一部分,进行测试,并确保所做的更改不会降低系统的其余部分。或者,如果这样,您至少可以看到巨大的纠结的混乱正在发展,而不是在释放时感到惊讶。

尽管您可以将一个方法拆分为三个,但是它们仍然会做与前一个方法相同的操作,因此您可以对旧方法进行测试,然后将其拆分为三个。编写第一项测试的工作不会浪费。

同样,将遗留系统的知识视为“临时知识”也不会很顺利。当涉及到遗留系统时,了解它以前是如何做的至关重要。对于“为什么会那样做?”这个古老的问题非常有用。


我想我了解,但是您在界面上迷失了我。即我正在编写的测试,在调用被测方法之后,检查某些变量是否已正确填充。如果这些变量被更改或重构,我的测试也将如此。我正在使用的现有旧类没有每个seh的接口/获取器/设置器,这将使变量更改或减少工作量。但是,对于旧式代码,我不确定您所说的接口是什么意思。也许我可以创造一些?但这将被重构。
丹尼斯

1
是的,如果您拥有一门能完成所有工作的神职人员,那么实际上没有任何接口。但是,如果它调用另一个类,则顶级类期望它以某种方式运行,并且单元测试可以验证它是否这样做。不过,我不会假装您在重构时不必更新单元测试。
菲利普(Philip)

4

我自己的答案/实现:

通过在重构时修复各种错误,我意识到,如果没有测试,我将不会轻易完成代码移动。测试使我意识到通过更改代码而引入的行为/功能“差异”。

当您进行了良好的测试时,您不必非常了解。您可以以更轻松的举止来编辑代码。测试会为您进行验证和健全性检查。

另外,我的测试与重构后的测试几乎一样,没有被破坏。实际上,当我深入研究代码时,我已经注意到了一些额外的机会来将断言添加到测试中。

更新

好吧,现在我要大量更改测试:/因为我重构了原始函数(删除了该函数,而是创建了一个新的cleaner类,将原来位于函数内部的绒毛移到了新类之外),所以现在我之前运行的被测代码使用不同的类名输入不同的参数,并产生不同的结果(带有绒毛的原始代码具有更多要测试的结果)。因此,我的测试需要反映这些变化,并且基本上,我正在将测试重写为新的东西。

我想还有其他解决方案可以避免重写测试。例如,用新代码保留旧函数名称,并在其中包含绒毛...但是我不知道这是否是最好的主意,我还没有足够的经验来判断该怎么做。


听起来更像是您重新设计了应用程序并进行了重构。
JeffO 2014年

何时重构以及何时重新设计?也就是说,在重构时,很难不将较大的笨拙的类分解为较小的笨拙的类,并且还要四处移动。因此,是的,我不确定该区别,但也许我两者都在做。
丹尼斯

3

使用测试来驱动代码。在旧版代码中,这意味着为要更改的代码编写测试。这样,它们就不是单独的工件。测试应该是关于代码需要实现的内容,而不是关于代码如何实现的内在胆量。

通常,您要在没有代码的代码上添加测试)以重构代码,以确保代码行为继续按预期运行。因此,在重构时连续运行测试套件是一个了不起的安全网。没有测试套件来更改代码以确认更改不会影响未预料到的事情的想法令人恐惧。

至于更新旧的测试,编写新的测试,删除旧的测试等的艰辛,我只是将其视为现代专业软件开发成本的一部分。


您的第一段似乎是在提倡忽略步骤1并在他进行过程中编写测试。您的第二段似乎与此矛盾。
pdr

更新了我的答案。
Michael Durrant

2

在您的特定情况下重构的目标是什么?

出于对我的回答的推定,我们都(一定程度上)相信TDD(测试驱动开发)。

如果重构的目的是在不更改现有行为的情况下清理现有代码,那么在重构之前编写测试就是确保未更改代码行为的方式,如果成功,则测试前后都会成功你重构。

  • 这些测试将帮助您确保您的新作品切实可行。

  • 测试可能还会发现原始作品无效的情况

但是,您如何真正进行任何重要的重构而又不会在某种程度上影响行为?

以下是重构期间可能发生的一些事情的简短列表:

  • 重命名变量
  • 重命名功能
  • 增加功能
  • 删除功能
  • 将功能分为两个或多个功能
  • 将两个或多个功能合并为一个功能
  • 分组课
  • 结合类
  • 重命名课程

我要争辩说,列出的活动中的每一项都会以某种方式改变行为

我要争论的是,如果您的重构改变了行为,那么您的测试仍然将是确保您没有破坏任何东西的方式。

也许行为不会在宏级别上发生变化,但是单元测试的目的并不是要确保宏行为。那就是集成测试。单元测试的重点是确保构建产品时所用的零碎零碎。链,最薄弱的环节等

这种情况如何:

  • 假设你有 function bar()

  • function foo() 打电话给 bar()

  • function flee() 也打电话给函数 bar()

  • 仅出于多样性,flam()请致电foo()

  • 一切工作出色(显然,至少)。

  • 您重构...

  • bar() 被重命名为 barista()

  • flee() 改为通话 barista()

  • foo()不是改叫barista()

显然,你的这两个测试foo()flam()现在失败。

也许您一开始就没有意识到foo()打来电话bar()。你肯定不知道,flam()是依赖于bar()通过的方式foo()

随你。问题的关键是,你的测试将揭示两者的新破碎行为foo()flam()你的重构工作中以增量方式。

这些测试最终可以帮助您良好地重构。

除非您没有任何测试。

这是一个人为的例子。有些人会认为,如果更改bar()中断了foo(),那么foo()从一开始就太复杂了,应该将其分解。但是过程由于某种原因能够调用其他过程,因此不可能消除所有复杂性,对吗?我们的工作是合理地管理复杂性。

考虑另一种情况。

您正在建造建筑物。

您建立一个脚手架,以帮助确保正确地建造建筑物。

脚手架可帮助您构建电梯井等。之后,您拆除了脚手架,但电梯井道仍然存在。您通过破坏脚手架破坏了“原始工作”。

这个类比是脆弱的,但重点是构建帮助您构建产品的工具并非闻所未闻。即使这些工具不是永久性的,它们也很有用(甚至是必要的)。木匠一直在做夹具,有时只做一份工作。然后,他们将夹具拆开,有时使用零件为其他工作制造其他夹具,有时不使用。但这并不会使夹具变得毫无用处或浪费。

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.