可维护的单元测试

25
我最近完成了一个使用TDD的项目,但是这个过程让我感到有点噩梦。我喜欢先编写测试并观察我的代码增长,但是一旦需求开始改变并且进行重构,我发现我花费的时间更多地用于重写/修复单元测试,实际上要花费更多的时间。
在我经历这个过程时,我觉得在应用程序完成后做测试会更容易,但是如果这样做,我将失去TDD的所有好处。
那么,有没有编写可维护的TDD代码的提示/技巧?我目前正在阅读Roy Osherove的The Art Of Unit Testing,还有其他资源可以帮助我吗?
谢谢

2
@Lee Tevail:你是在说(a)你的IDE没有全局搜索和替换功能 (b) TDD 导致你的IDE很差? - S.Lott
1
我主要使用 VS,但其他 IDE 也具有某种形式的替换/重命名功能,可以全局代表您完成工作。 - Finglas
4
“那只是一个例子”,这并不是TDD存在问题的很好的例子。听起来更像是你的IDE(或者你对IDE的使用)出了问题,而不是TDD本身有问题。对你来说,TDD是有效的。 - S.Lott
好的,但这个问题是TDD的副作用,我相信如果我观察一个TDD专家几天,我会学到很多技巧。你说得对,这更多是我的IDE或者我使用它的方式的问题,我应该换一种方式来提问。 - Lee Treveil
@Lee Treveil:“这个问题是TDD的副作用”。实际上,它与之无关。“如果我观看一位TDD专家几天,我会学到很多技巧。”我不太相信。如果您花费的时间比编码还要多在测试上,那么您做得非常好。 - S.Lott
显示剩余6条评论
11个回答

17

实践

学习撰写良好的单元测试需要一定时间。一个困难的项目(更像是多个项目)并不奇怪。

已经推荐的xUnit Test Patterns书籍很好,我听说你正在阅读的那本书也不错。

至于一般性建议,这取决于测试中出现了哪些问题。如果它们经常出错,它们可能不是单元测试,而更像是集成测试。如果设置困难,则SUT(被测试系统)可能显示出过于复杂,并需要进一步模块化。清单还可以继续。

我遵循的一些建议是遵循AAA规则。

安排,执行和断言。每个测试都应该遵循这个公式。这使得测试易读,并且在需要维护时易于维护。

设计仍然很重要

我实践TDD,在编写任何代码之前,我会拿起白板涂鸦。虽然TDD允许您的代码逐步发展,但一些前期设计总是有益的。然后你至少有一个起点,从这里你的代码可以由你编写的测试驱动。

如果我正在执行特定的困难任务,我会制作一个原型。忘记TDD,忘记最佳实践,只是批量处理一些代码。显然,这不是生产代码,但它提供了一个起点。从这个原型开始,我再考虑真正的系统以及我需要什么测试。

查看Google Testing Blog-这是在开始TDD时对我自己的转折点。 Misko的文章(和网站-尤其是关于可测试代码的指南)非常好,并且应该指引您朝着正确的方向。


+1. 喜欢对 Misko 文章的引用。我认为它们也是我一个转折点。 - Lieven Keersmaekers

7

"当需求开始改变,我开始进行重构时,我发现我花费的时间在重写/修复单元测试上比写代码的时间还要多,事实上多得多。"

那么?这是一个问题吗?

你的需求改变了。这意味着你的设计必须改变。这意味着你的测试也必须改变。

"我花费的时间在重写/修复单元测试上比写代码的时间还要多,事实上多得多。"

这意味着你做得很对。需求、设计和测试的影响都在测试中,而你的应用程序并不需要太多的更改。

这就是它应该工作的方式。

回家高高兴兴。你已经做好了工作。


这样的思维方式正是我不接受TDD的原因。程序员的工作不是编写测试,而是产生可工作、优秀的代码。如果验证代码是否良好的时间比实际编写代码的时间更长,那么我认为这个过程中存在严重的缺陷。 - erikkallen
8
程序员的工作是“编写运行良好的代码”,并且您需要对其有“完全的信心”。我无法强调“完全的信心”这一点。测试是提高代码信心的好方法。验证代码是否良好应该需要很长时间。TDD并不改变这种情况,没有任何东西可以改变这种情况。信心需要很多关注--要么进行大量的测试,要么建立一个精心构建的证明,或者两者都需要。 - S.Lott

5
我非常喜欢单元测试,但在最近的项目中遇到了TDD(或基本单元测试)的问题。在进行实施后评估后,我发现我们(我和团队的其他成员)在TDD和单元测试的实施/理解方面遇到了两个主要问题。
第一个问题是我们并不总是把测试视为一等公民。我知道这听起来像是我们违反了TDD的哲学,但我们的问题是在完成大部分初始设计并被迫进行即时更改之后出现的。不幸的是,由于时间限制,项目的后半部分变得匆忙,并且我们陷入了在编写代码后编写测试的陷阱。随着压力的增加,工作代码被检入源代码控制而没有检查单元测试是否仍然通过。诚然,这个问题与TDD或单元测试无关,而是紧张的截止日期、平均团队沟通和糟糕的领导(我要在这里责怪自己)的结果。
当深入研究失败的单元测试时,我们发现我们测试了太多东西,特别是考虑到我们的时间限制。我们使用TDD并为整个代码库编写测试,而不是专注于具有高回报的代码。这使我们的单元测试比例比我们能够维护的要高得多。我们(最终)决定仅使用TDD并为可能更改的业务功能编写测试。这减少了我们需要维护大量测试的需求,这些测试在很大程度上很少(或从不)更改。相反,我们的努力更加集中,使我们真正关心的应用程序部分的测试套件更加全面。
希望您可以从我的经验中学到,并继续开发TDD,或者至少为您的代码开发单元测试。就我个人而言,我发现以下链接在帮助我理解选择性单元测试等概念方面非常有用。

3
听起来你的单元测试很脆弱且重叠。理想情况下,一次代码更改应该只影响一个单元测试 - 一对一匹配测试和功能,其他测试不依赖于特定功能。这可能有点理想化;在实践中,我们的许多测试确实会重新运行相同的代码,但这是需要牢记的。当一个代码更改影响多个测试时,这是一个问题。此外,关于您具体的重命名示例:找到一个可以为您自动化这些重构的工具。我相信Resharper和CodeRush都支持这种自动化重构; 这比手动方法更快,更容易,更可靠。
要更好地学习您的IDE,没有什么比与其他人配对更好的了。你们两个都会学习; 您们都会开发新技能 - 而且不需要花费太长时间。几个小时将大大增加您使用该工具的舒适度。

+1 推荐配对学习 IDE。我喜欢每隔几周花半天时间,专门学习关于我的 IDE 或其他工具的新知识。长期来看,这会带来巨大的回报。 - George Armhold

2
是的,有一本名为xUnit Test Patterns的完整书籍专门处理这个问题。
这是Martin Fowler的代表作之一,因此它具备经典模式书籍的所有特点。你是否喜欢这样的书籍取决于个人口味,但我个人认为它非常有价值。
总之,问题的要点是你应该像对待生产代码一样对待测试代码。首先,你应该遵循DRY原则,因为这使得重构API更容易。

2

你是否广泛使用接口、依赖注入和模拟?

我发现,设计接口并使用像ninject这样的DI框架注入这些接口的实现,可以更轻松地模拟应用程序的部分内容,以便正确地测试各个组件。

这样,就可以更轻松地在一个区域进行更改,而不会对其他区域产生太大影响,或者如果确实需要传播更改,则只需更新接口,并逐个处理每个不同的区域。


2
我认为你需要在测试和编码之间取得适当的平衡。
当我开始一个项目时,由于需求和目标经常变化,我几乎不写任何测试,因为如你所观察到的,不断修复测试需要太多时间。有时候我只是在注释中写上“这应该被测试”,以便我不会忘记测试它。
在某个时刻,你会感觉到你的项目正在成形。这时重度单元测试就派上用场了。我尽可能多地编写测试。
当我开始进行大量重构时,我不会过多关注测试,直到项目再次稳定下来。我还会放一些“测试这个”的注释。当重构完成后,就是重新编写所有失败的测试的时候了(也许还要放弃其中一些测试,并且肯定要编写一些新的测试)。
以这种方式编写测试真的很愉快,因为它表示你的项目已经达到了一个里程碑。

1
首先,重构不会破坏单元测试。你是否根据这本书应用了重构?可能是因为你的测试在测试具体实现而非行为,这可能解释了为什么它们会失效。
单元测试应该是黑盒测试,测试单位的功能,而不是它的具体实现方式。

1
我发现了一个名为SpryTest的Java半自动化开发者测试工具。它提供了一个简单而强大的用户界面来创建随机数据。它还支持使用powermock和easymock进行模拟调用。它可以生成最接近手写测试的标准JUnit测试。它还有测试->源代码同步功能。
我试用了一下,效果不错。请访问http://www.sprystone.com查看该工具。

0

你使用的是好的集成开发环境吗?几年前,当我第一次接触单元测试时,我也问过自己同样的问题。那时,我使用的是Emacsfindgrep的组合来进行重构。这很痛苦。

幸运的是,我的一个同事敲了我一下脑袋,并说服我尝试使用“现代工具”,在他的话中意味着Intellij IDEA。IDEA是我个人的首选,但Netbeans或Eclipse同样可以处理基本操作。这为我带来了难以言喻的生产力提升;特别是对于有大量测试的大型项目,效果更加显著。

一旦你选择了一个IDE,如果你仍然遇到问题,那么就该考虑DRY原则了。该原则旨在确保信息只保存在一个地方(常量、属性文件等),以便以后需要更改时最小化连锁反应。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接