28

我听说使用 TDD 开发的项目更容易重构,因为这种做法产生了一套全面的单元测试,如果任何更改破坏了代码,这些测试将(希望)失败。然而,我所看到的所有示例都涉及重构实现——例如,用更有效的算法更改算法。

我发现重构架构在设计仍在制定的早期阶段更为常见。接口发生变化,添加和删除新类,甚至函数的行为也可能略有变化(我以为我需要它来做这个,但它实际上需要做那个)等等......但是如果每个测试用例都是紧密耦合的对于这些不稳定的类,您不是每次更改设计时都必须不断地重写您的测试用例吗?

在 TDD 中什么情况下可以更改和删除测试用例?您如何确保更改测试用例不会破坏它们?另外,似乎必须将全面的测试套件与不断变化的代码同步会很痛苦。我知道单元测试套件可以在维护期间提供巨大帮助,一旦软件构建、稳定和运行,但这是在游戏后期,而 TDD 也应该在早期提供帮助。

最后,一本关于 TDD 和/或重构的好书会解决这些问题吗?如果是这样,你会推荐哪个?

4

8 回答 8

10

您需要记住的一件事是,TDD 主要不是一种测试策略,而是一种设计策略。您首先编写测试,因为这有助于您提出更好的解耦设计。更好的解耦设计也更容易重构。

当您更改类或方法的功能时,测试也必须更改,这是很自然的。事实上,遵循 TDD 意味着您首先更改测试,当然。如果您必须更改大量测试以仅更改单个功能,这通常意味着大多数测试都过度指定了行为 - 它们测试的内容超出了应测试的范围。另一个问题可能是责任没有很好地封装在您的生产代码中。

不管是什么,当你因为一个小改动而经历许多测试失败时,你应该重构你的代码,这样它就不会在将来再次发生。总是有可能做到这一点,尽管并不总是显而易见的。

随着更大的设计更改,事情可能会变得更加复杂。是的,有时编写新测试并丢弃旧测试会更容易。有时,您至少可以编写一些集成测试来测试整个重构部分。并且您希望仍然拥有您的验收测试套件,这些测试大多不受影响。

我还没有读过它,但是我听说过《XUnit 测试模式 - 重构测试代码》这本书的好东西。

于 2008-11-01T20:41:54.040 回答
8

另外,似乎必须将全面的测试套件与不断变化的代码同步会很痛苦。我知道单元测试套件可以在维护期间提供巨大帮助,一旦软件构建、稳定和运行,但这是在游戏后期,而 TDD 也应该在早期提供帮助。

我确实同意在这些早期变化中可以感受到单元测试套件的开销,当主要的架构变化发生时,但我认为单元测试的好处远远超过这个缺点。我经常认为问题出在心理问题上——我们倾向于将单元测试视为代码库的二等公民,我们讨厌不得不与它们打交道。但随着时间的推移,随着我开始依赖它们并欣赏它们的有用性,我开始认为它们与代码库的任何其他部分一样重要,也同样值得维护和工作。

主要的架构“变化”真的只是重构吗?如果您只是重构,无论多么显着,并且测试开始失败,那可能会告诉您您无意中更改了某处的功能。这正是单元测试应该帮助你抓住的。如果您同时对功能和架构进行彻底的更改,您可能需要考虑放慢速度并进入红/绿/重构槽:没有新的(或更改的)功能没有额外的测试,也没有更改重构时的功能(和中断测试)。

更新(基于评论):

@Cybis 对我的主张提出了一个有趣的反对意见,即重构不应该破坏测试,因为重构不应该改变行为。他的反对意见是重构确实会改变 API,因此会测试“中断”。

首先,我鼓励任何人访问有关重构的规范参考:Martin Fowler 的 bliki。刚才我回顾了它,有几件事突然出现在我身上:

  • 改变接口重构? Martin 将重构称为“行为保持”更改,这意味着当接口/API 更改时,该接口/API 的所有调用者也必须更改。我说,包括测试。
  • 这并不意味着行为已经改变。Fowler 再次强调,他对重构的定义是更改是 保持行为的。

鉴于此,如果一个或多个测试在重构期间必须更改,我不认为这是“破坏”测试。它只是重构的一部分,保留了整个代码库的行为。我认为必须更改的测试与作为重构的一部分必须更改的代码库的任何其他部分之间没有区别。(这可以追溯到我之前所说的将测试视为代码库的一等公民。)

此外,我希望测试,甚至是修改后的测试,在重构完成后继续通过。无论该测试正在测试什么(可能是该测试中的断言)在重构完成后都应该仍然有效。否则,这是一个危险信号,在重构过程中行为以某种方式改变/退化。

也许这种说法听起来像是胡说八道,但仔细想想:我们对在生产代码库中移动代码块并期望它们继续在新的上下文中工作(新类、新方法签名等)没有任何想法。我对测试也有同样的感觉:也许重构改变了测试必须调用的 API,或者测试必须使用的类,但最终测试点不应该因为重构而改变。

(我能想到的唯一例外是测试您可能希望在重构期间更改的低级实现细节的测试,例如用 ArrayList 或其他东西替换 LinkedList。但在这种情况下,人们可能会争辩说测试过度测试并且过于僵化和脆弱。)

于 2008-11-02T22:09:06.610 回答
7

TDD 给重构带来的主要好处是开发者有更多的勇气去改变他们的代码。准备好单元测试后,开发人员敢于更改代码然后运行它。如果 xUnit 条仍然是绿色的,他们就有信心继续前进。

就个人而言,我喜欢 TDD,但不鼓励过度 TDD。也就是不要写太多的单元测试用例。单元测试应该足够了。如果您过度单元测试,那么当您想要进行架构更改时,您可能会发现自己处于两难境地。生产代码中的一个大变化会带来大量单元测试用例的变化。所以,只要保持你的单元测试足够。

于 2008-11-01T04:04:19.483 回答
4

TDD 说先写一个失败的测试。编写测试是为了表明开发人员了解用例/故事/场景/流程应该实现的目标。

然后编写代码来满足测试。

如果需求发生变化或被误解,请先编辑或重写测试。

红条,绿条,对吧?

Fowler's Refactoring是重构参考,很奇怪。

Scott Ambler 在Dobb 博士(“敏捷边缘??”)中的系列文章是 TDD 在实践中的一个很好的演练。

于 2008-11-01T03:58:46.017 回答
3

例如,用更有效的算法更改算法。

这不是重构,这是性能优化。重构是关于改进现有代码的设计。即改变其形状以更好地满足开发者的需求。以影响外部可见行为为目的而更改代码不是重构,这包括为提高效率而进行的更改。

TDD 的部分价值在于,您的测试可以帮助您保持可见行为不变,同时改变产生结果的方式。

于 2008-11-01T06:08:05.427 回答
2

我会推荐(和其他人一样):

于 2008-11-01T04:53:59.257 回答
1

Kent Beck 的 TDD 书籍。

先测试。如果不需要,遵循 SOLID OOP 原则和使用良好的重构工具是必不可少的。

于 2008-11-01T04:21:28.527 回答
1

在 TDD 中什么情况下可以更改和删除测试用例?您如何确保更改测试用例不会破坏它们?另外,似乎必须将全面的测试套件与不断变化的代码同步会很痛苦。

测试和规范的重点是定义系统的正确行为。所以,非常简单:

if definition of correctness changes
  change tests/specs
end

if definition of correctness does not change
  # no need to change tests/specs
  # though you still can for other reasons if you want/need
end

因此,如果应用程序/系统规范或期望的行为发生变化,则有必要更改测试。在这种情况下仅更改代码而不更改测试显然是破坏了方法论。您可能将其视为“痛苦”,但没有测试套件会更痛苦。:) 正如其他人所提到的,拥有“敢于”更改代码的自由确实是非常授权和解放的。:)

于 2008-11-01T04:58:15.583 回答