【发布时间】:2011-09-07 23:19:00
【问题描述】:
我最近(上周)开始了一项实验,我尝试在我正在使用 TDD 原则从事的项目中编写新功能。过去,我们的方法是一种适度敏捷的方法,但没有非常严格。方便时,单元测试会在这里和那里进行。全面测试覆盖的主要障碍是我们的应用程序具有复杂的依赖关系网络。我选择了一个方便隔离的功能来尝试我的实验;细节并不重要,可能商业敏感,足以说这是一个简单的优化问题。
到目前为止,我发现:
- 对我来说,TDD 似乎鼓励漫无目的、不明显的设计成形。未经测试不得编写代码的限制往往会阻碍将功能分解为独立单元的机会。在实践中,同时为这么多功能进行思考和编写测试太难了
- TDD 倾向于鼓励创建可以做所有事情的“上帝对象”——因为您已经为 x 类编写了很多模拟类,但为 y 类编写的模拟类很少,所以在当时看来,x 类也应该是合乎逻辑的实现特征 z,而不是将其留给 y 类。
- 在编写代码之前编写测试要求您在解决问题之前对问题的每一个复杂性有一个完整的了解。这似乎是一个矛盾。
- 我无法让团队支持开始使用模拟框架。这意味着专门为测试特定功能而创建的杂乱无章的扩散。对于每个测试的方法,您往往需要一个假的,它唯一的工作就是报告被测类调用了它应该调用的任何东西。我开始发现自己编写类似于 DSL 的东西纯粹是为了实例化测试数据。
- 尽管存在上述问题,但与我习惯的开发模式不同,TDD 产生了一个几乎没有神秘错误的工作设计。然而,重构导致的混乱局面需要我暂时放弃 TDD 并完成它。我相信测试将继续强制执行该方法的正确性。尝试对 TDD 进行重构,我觉得这只会让事情变得更加繁琐。
那么问题是“是否有人有任何建议可以减少上述问题的影响?”。我毫不怀疑模拟框架将是有利的。但是目前我已经在尝试一些似乎只会产生杂乱无章的代码的东西。
编辑#1:
感谢大家深思熟虑的回答。我承认我在星期五晚上喝了几杯啤酒后写了我的问题,所以在某些地方它是模糊的,并没有真正表达我真正想要的情绪。我想强调一下,我确实喜欢 TDD 的哲学,并且发现它相当成功,但由于我列出的原因也令人惊讶。下周我有机会睡在上面,用新的眼光再次审视这个问题,所以也许我可以通过蒙混过关来解决我的问题。但是,它们都不是初学者。
更让我担心的是,一些团队成员不愿意尝试任何你可以称之为“技术”的东西,而倾向于“完成它”。我担心 cruft 的出现会被视为对该过程的一个黑标,而不是证明它需要完全完成(即使用模拟框架和强大的 DI)以获得最佳结果。
RE “TDD 不一定意味着测试优先”:(womp, btreat)
我在该问题上找到的每篇文章中的“黄金法则”都是“红色、绿色、重构”。那就是:
- 编写一个必须失败的测试
- 编写通过测试的代码
- 重构代码,使其以最实用的方式通过测试
我很好奇人们如何想象在不遵循最初编写的 TDD 核心原则的情况下进行测试驱动开发。我的同事称中途之家(或不同且同样有效的方法,取决于您的观点)“测试-验证开发”。在这种情况下,我认为创造一个新术语 - 或者可能从其他人那里窃取并以此为荣 - 很有用。
用于测试数据的 RE DSL:(Michael Venable)
我很高兴你这么说。我确实看到通用形式在整个项目范围内变得越来越有用,因为所讨论的应用程序维护着一个非常复杂的对象图,并且通常,测试它意味着运行应用程序并在 GUI 中进行尝试。 (出于上述商业敏感性的原因,不会放弃游戏,但它从根本上与优化有向图上的各种指标有关。但是,其中涉及许多警告和用户可配置的小部件。)
能够以编程方式设置有意义的测试用例将有助于各种情况,可能不仅限于单元测试。
RE 神物:
我有这种感觉是因为一个班级似乎占据了大部分功能集。也许这很好,它确实那么重要,但它引起了一些人的注意,因为它看起来就像不是按照这些路线开发的旧代码,并且似乎违反了 SRP。我想某些类将不可避免地主要作为许多不同封装的功能位之间的接缝而其他类只会接缝少数。如果是这样的话,我想我需要做的是从这个明显的上帝对象中清除尽可能多的逻辑,并将其行为重新塑造为所有分解部分之间的连接点。
(对版主:我已在此处添加对帖子的回复,因为评论字段不够长,无法包含我想要的详细信息。)
编辑 #2(大约五个月后):
嗯,我觉得在考虑了一段时间后,更新一些想法可能会很好。
很遗憾,我最终还是放弃了 TDD 方法。不过,我觉得这其中有一些具体且合理的理由,并且我已经准备好下次有机会时继续这样做。
TDD 毫无歉意的重构心态的一个结果是,当我简要查看我的代码时,我并没有感到非常沮丧,主要开发人员宣称其中的绝大多数都是毫无意义的,需要删除。虽然不得不放弃大量的辛勤工作感到有些遗憾,但我明白他的意思。
出现这种情况是因为我从字面上理解了“代码到接口”的规则,但继续编写试图代表现实的类。很久以前我第一次发表声明:
类不应试图代表现实。对象模型应该只尝试解决手头的问题。
...从那以后我尽可能多地重复;对我自己和任何愿意倾听的人。
这种行为的结果是一个执行功能的类的对象模型,以及一组重复类功能的镜像接口。向我指出了这一点,经过短暂但强烈的抵抗后,看到了曙光,删除大部分内容没有问题。
这并不意味着我相信“接口代码”是无稽之谈。它的意思是,当接口代表真实的业务功能时,对接口的编码主要是有价值的,而不是一些想象中的完美对象模型的属性,它看起来像现实生活的缩影,但不考虑它的唯一含义。生活要回答你最初提出的问题。 TDD 的优势在于它无法生成这样的模型,除非是偶然。由于它从提出问题开始并且只关心获得答案,因此不会涉及您的自我和对系统的先验知识。
我现在正在胡言乱语,所以我最好完成这件事,并声明我非常渴望再次尝试 TDD,但对可用的工具和策略有更好的了解,并将尽我所能在开始之前决定我想怎么做。也许我应该把这个华夫饼移植到它所属的博客上,一旦我有更多话要说。
【问题讨论】:
-
我目前正在阅读:Growing Object-Oriented Software Guided by Tests。关于这个主题的优秀读物。
-
我有 Professional Test-Driven Development with C#,一些 cmets 认为这是衍生作品。我选择它是因为它特定于 C# 并且使用了一些我以前看过的工具(NUnit、Ninject)。
-
Osherove 的 The Art Of Unit Testing 相当不错,虽然它并不特定于 TDD-only。
-
关于您的编辑:您应该遵循的一个总体原则是“YAGNI”——“您不需要它”。您编写的每一行代码都应该有助于促进现有系统的发展。永远不要构建您以后“可能”需要的东西,因为您可能不需要,即使您这样做了,您可能仍然需要更改您认为需要使其以您现在确实需要的方式工作的东西。对接口进行编码是一种很好的“象牙塔”实践,但使用 ReSharper 之类的工具提取接口是一分钟的操作,因此实际上,在需要时进行。