如何避免使用TDD创建糟糕的设计

时间:2011-06-03 16:36:47

标签: tdd mocking

7 个答案:

答案 0 :(得分:11)

TDD并非嘲笑。有时,嘲讽有助于开发测试,但如果在TDD的第一次通过中你开始使用模拟,你可能没有得到最好的实践介绍。

根据我的经验,TDD没有导致上帝的物体;恰恰相反。 TDD将我带到了能够做更少事情并与更少其他类交互的类,更少的依赖。

  

一个人不得的限制   编写没有测试的代码往往   阻止机会分解   功能分为独立单元。   为此努力并编写测试   同时也有很多功能   在实践中很难。

这对我来说听起来不太合适。您不是要同时为许多功能编写测试;您正尝试一次为一个功能编写一个测试。编写该测试后,即可通过。当它通过时,你会让它变得干净。然后你编写下一个测试,可能会推动相同功能的进一步开发,直到功能完成,并清理。然后为下一个功能编写下一个测试。

  

在编写代码之前编写测试   要求你有一个完整的   了解每一个复杂的   解决问题之前的问题。这个   似乎是一个矛盾。

再次:写一个测试。这需要完全理解一个功能的一个方面。它需要它,并以可执行的形式具体表达它。

答案 1 :(得分:7)

正如对您遇到的问题的一揽子回应一样,听起来您已经很长时间没有使用TDD,您可能没有使用任何可能有助于TDD流程的工具,而且您和#39;在生产代码行上重新投入比测试代码行更多的价值。

更具体地说,每一点:

1:TDD鼓励设计不会超过或低于其所拥有的设计,即a" YAGNI" (你不会需要它)的方法。那个"做得很轻"。您必须与"做正确的"进行平衡,即将适当的SOLID设计概念和模式合并到系统中。我采用以下经验法则:在第一次使用一行代码时,让它工作。在对该行的第二个引用上,使其可读。第三,让它变得坚固。如果一行代码仅由另一行代码使用,那么在那时放入完全SOLID设计并将代码分解为可以插入的接口抽象类是没有意义的。并换掉了。但是,一旦开始获得其他用途,您必须具备回溯并重构代码的规则。 TDD和敏捷设计完全是关于重构的。这就是摩擦;瀑布也是如此,它只需花费更多,因为你必须一直回到设计阶段才能做出改变。

2:再次,这是纪律。单一责任原则:一个对象应该做一个特定的事情,并且是系统中唯一做这件事的对象。 TDD不允许你懒惰;它只是帮助你找到你可以懒惰的地方。此外,如果你需要创建一个类的许多部分模拟,或者许多功能强大的完整模拟,你可能会错误地构建对象和测试;你的对象太大,你的SUT有太多的依赖关系,和/或你的测试范围太宽。

3:不,它没有。它要求您在编写测试套件时考虑所需的内容。在这里,像ReSharper(MSVS)这样的重构助手真的很闪耀; Alt + Enter是你的"做它"捷径。让我们假设您正在编写一个新类,它将写出一个报告文件。你做的第一件事是新建一个该类的实例。 "等等",ReSharper抱怨,"我找不到那个班级!"。 "所以创建它",你说,按Alt + Enter。它就这样做了;你现在有一个空的类定义。现在,您在测试中编写了一个方法调用。 "等待," ReSharper哭了,"这种方法不存在!",你说"然后创建它"再按Alt + Enter键。您只是按测试编程;你有新逻辑的骨架结构。

现在,您需要一个要写入的文件。首先输入一个文件名作为字符串文字,知道当RS抱怨时你可以简单地告诉它将参数添加到方法定义中。等等,那不是单元测试。这需要您创建的方法来触摸文件系统,然后您必须返回文件并通过它以确保它是正确的。所以,你决定通过一个Stream;它允许您传入一个完全与单元测试兼容的MemoryStream。 其他 TDD影响设计决策;在这种情况下,决定是让课程更加SOLID,以便进行测试。同样的决定使您可以灵活地将数据传输到您希望的任何地方;进入内存,文件,网络,命名管道等等。

4:敏捷团队通过协议进行计划。如果没有协议,那就是一个块;如果团队被阻止,则不应编写任何代码。要解决阻止,团队负责人或项目经理会做出命令决策。在证明错误之前,这个决定是对的;如果它最终错了,它应该快速完成,这样团队就可以朝着新的方向前进,而不必回溯。在您的具体情况下,让您的经理做出决定 - 犀牛,Moq,无论如何 - 并执行它。其中任何一个都比手写测试模拟好一千倍。

5:这应该是TDD的真正优势。你有一个班级;它的逻辑是一团糟,但它是正确的,你可以通过运行测试证明它。现在,您开始重构该类以获得更多SOLID。如果重构没有改变对象的外部接口,那么测试甚至不必改变;你只是清理一些测试不关心的方法逻辑,除非它有效。如果您要更改界面,则更改测试以进行不同的调用。这需要纪律;因为正在测试的方法不存在,所以很容易将一个不再起作用的测试放入其中。但是,您必须确保对象中的所有代码仍然得到充分运用。代码覆盖工具可以在这里提供帮助,它可以集成到CI构建过程中,并且可以打破构建过程。如果覆盖范围不高于鼻烟。然而,覆盖的另一面实际上是双重的:首先,为覆盖范围增加覆盖率的测试是没有用的;每个测试必须证明代码在某些新颖的情况下按预期工作。此外,"覆盖"不是"运动&#34 ;;您的测试套件可以执行SUT中的每一行代码,但它们可能无法证明逻辑线在每种情况下都能正常工作。

所有这一切,有一个非常有力的教训,当我第一次学习它时,TDD将会给我什么并且不会给我。这是一个编码道场;任务是写一个罗马数字解析器,它将采用罗马数字字符串并返回一个整数。如果你理解罗马数字的规则,这很容易预先设计,并可以通过任何给定的测试。但是,TDD规则可以非常容易地创建一个类,该类包含测试中指定的所有值及其整数的Dictionary。它发生在我们的道场。这就是摩擦;如果解析器的实际规定要求是它只处理我们测试的数字,那么我们没有做错任何事;系统"工作"我们并没有浪费任何时间来设计一些在一般情况下工作的更精细的东西。然而,我们新的Agilites看着泥潭,并说这种做法是愚蠢的;我们知道"它必须更聪明,更强大。但是我们呢?这是TDD的力量及其弱点;你可以设计一个满足用户声明要求的系统,因为你不应该(通常也不能)编写那些不符合或证明该人给你的系统要求的代码。支付账单。

虽然我做了很多后期开发测试,但这样做有一个很大的问题。您已经编写了生产代码,并希望以其他方式对其进行测试。如果现在测试失败了,谁错了?如果是测试,则更改测试以断言程序当前正在输出的内容是否正确。嗯,没用多少;你刚刚证明了系统输出它一直有的东西。如果它是SUT,那么你有更大的问题;你有一个你已经完全开发的对象没有通过你的新测试,现在你必须撕开它并改变它来做到这一点。如果这是您迄今为止对该对象的唯一自动测试,那么谁知道您将通过这一测试打破什么?相反,TDD强制您在合并任何将通过该测试的新逻辑之前编写测试,因此您具有回归验证代码;在开始添加新代码之前,您有一套测试证明代码符合当前要求。因此,如果在添加代码时现有测试失败,那么您就会破坏某些内容,并且您不应该为该版本提交该代码,直到它通过所有已经存在的测试和所有新测试。

如果测试中存在冲突,那就是一个阻止。假设您有一个测试证明给定的方法返回给定A,B和C的X.现在,您有一个新的要求,并且在开发测试时您发现现在相同的方法必须在给定A,B和时输出Y. C.那么,先前的测试对于证明系统以旧方式工作是不可或缺的,因此更改该测试以证明它现在返回Y可能会破坏基于该行为的其他测试。要解决此问题,您必须澄清新要求是旧行为的变化,还是从接受要求错误地推断出其中一个行为。

答案 2 :(得分:6)

我非常建议您继续使用您的方法,然后阅读Gerard Mezaros的书籍,xUnit测试模式,并尝试应用他的指南。 TDD是一条漫长而曲折的道路,开始看到它的好处需要相当长的一段时间。我对您的一些担忧的简短回答如下:

  • TDD确实鼓励非显而易见的设计。通常这些比明显的更好。你可以在网上找到的一些katas显示了这个功能 - 你可能已经想象了一整套类TDD结果只有很少的代码就会产生一两个。我不同意它阻止了分解比特的机会--TDD的口头禅是红色,绿色,重构。我认为这里的秘诀不是一次性考虑所有功能 - 在你开始考虑如何通过课程完成之前,从高级别一次坚持一个。
  • 可能就是这种情况,但是当你有自己的重构帽时,你总是可以将你的大班重构成多个班级,安全地知道你的考试会在你的重构中发现任何错误。 TDD鼓励在每个机会进行重构(只要你有绿灯),所以上帝的对象不应该是结果。
  • 我不同意这个。所有TDD要求的是你知道你需要的一件事并写下那个测试。然后你就可以通过了。然后你会想到你需要的另一件事。各种katas很好地说明了这一点。 Kent Beck关于TDD的原着书也说明了这一过程。
  • 嘲弄是困难的,这是事实。此外,用于设置测试数据的DSL是一个好主意:)
  • 据我所知,根据定义,重构不需要更多测试。你在绿色栏下重构 - 这意味着你永远不会为你的重构编写测试。重构可能会导致创建一个新类,此时您可能需要创建一个新的测试类来测试新类并将测试一次一个地移动到新的测试类,但通常重构是在没有增加了新的测试。

答案 3 :(得分:2)

我认为你陷入了普遍的误解,即TDD总是意味着“先测试”。测试优先开发不一定与TDD相同。 TDD是一种软件工程方法,专注于编写可测试代码。为了练习TDD,没有严格要求总是首先编写测试。

让我打破你的论点,希望我能帮助你清理一些障碍!

TDD for me seems to encourage rambling, non-obvious designs to take
     

形状。一个人必须的限制   没有测试就不会编写代码   阻止机会分解   功能分为独立单元。   为此努力并编写测试   同时也有很多功能   在实践中很难

TDD是关于尝试编写可测试代码的。首先编写测试的重点是,您可以修改您的类正在执行的操作,并确保您设计这些类以使它们可测试。

您的测试不应该是锁定您或强制您的设计。测试是可变的 - 如果你编写一个测试,稍后它会阻止你重构某些东西,只需删除测试。您不能编写工作代码来考虑已编写的测试。测试可以支持您的代码,而不是相反。

TDD tends to encourage the creation of 'God Objects' that do
     一切 - 因为你已经写好了   很多类x的模拟类   已经,但是对于y级来说很少,所以它   在类x时似乎合乎逻辑   还应该实现功能z   而不是把它留给y级。

I haven't been able to get the team on-side to start using a mocking
     

框架。这意味着有一个   仅仅创造了狂暴的扩散   测试特定功能。对于   每种方法都经过测试,你会倾向于   需要一个唯一的工作就是假的   报告被测试的课程   叫什么它应该。我   开始发现自己在写作   类似DSL的东西纯粹是为了   实例化测试数据。

应该使用框架来完成模拟,以消除编写所有这些模拟类的负担。同样,听起来您将测试视为静态,并编写实际代码以适应现有测试。这不是您面临的TDD问题 - 这是一个管理问题。这是你的团队正在创造弊端,而不是TDD流程。只是将一个特征移动到一个不属于它的类是一个有意识的选择,出于方便,而不是正确的工程。如果嘲笑是负担,那就是导致懒惰选择的问题。

Writing tests before you write code requires that you have a complete
     理解每一个复杂的问题   解决问题之前的问题。这个   似乎是一个矛盾。

测试应该与您的代码一起流畅地发展。在开始为它编写测试之前,没有要求理解所有内容 - 测试应该帮助您创建设计,以便创建良好的,可测试的类。如果你必须在几天内建立真正的代码,然后回来更新你的测试,TDD警察就不会破坏你的门。关键是,在构建课程时,您正在考虑如何测试它们。同时编写测试很好,但有时你还不知道写它们。测试需要与您合作,而不是与您合作。

记住 - “先测试”只是做TDD的一种方式。这不是唯一的方法,事实上,我从来没有见过任何人为他们编写的每一段代码实行“先测试”。

答案 4 :(得分:1)

由于您提到的原因,我个人不喜欢在代码之前编写测试。在实践中,我更喜欢遵循编写代码的方法,编写代码测试,确保测试运行,然后提交代码和测试。这样可以避免您提到的一些问题,同时保留TDD旨在促进的一些好处。编写代码的方式是为了方便最终要进行的测试,但代码驱动测试而不是相反。

答案 5 :(得分:1)

我认为从TDD开始你的感觉是正常的。您的代码必须遵循某些约定来支持TDD(如依赖注入),并且改变您的代码编写风格以支持它随着时间的推移而发生。

我发现自己在为测试编写DSL时并不感到惊讶,我认为这不是一件坏事。它使测试更容易阅读,这将有助于维护测试。我希望它能让你的同事更容易添加他们自己的测试。最近,我发现自己使用fluent interface来使测试代码可读并将常见逻辑分解到一个地方。

示例:

  LoadModelSetupFromTestFileCollection("VariableSetToScript.xml");
  AssertVariable("variable").HasValue(3);

测试代码的每个功能都是很多工作,但我不知道另一种自信地说你知道所有代码都有效的方法。由于未来的代码更改会破坏现有代码,因此从长远来看,拥有一组自动化测试确实可以获得回报。

我能给出的最佳建议:

  • 选择单位是什么。它不一定是一个阶级。一个小模块(有几个类)可以作为被测单元。这样做可以减少模拟的需要,因为您一次测试了一组类。唯一的缺点是调试更困难,但您的测试同样有效。

  • 我喜欢在代码之前编写测试,但有时候,你真的需要先探索一下这些想法。可以在需要时编写更多“探索性”代码并稍后编写测试。但是,我怀疑大多数时候,在开始编写代码之前,您已经很好地了解了所需的功能。

  • 将测试代码纳入函数。我做了代码更改,几乎没有影响生产代码,但通过测试代码引起了波纹效应。精心设计的测试代码可以解决这个问题,并有助于维护。

  • 类似于类似DSL的语言的测试代码在维护方面有很长的路要走,让IMO更容易创建新的测试。

我认为你走在正确的轨道上。如果你很难说服人们嘲笑,那就不要求了。如果您觉得测试每个功能都太多,那么请测试最有可能失败的功能。从小处开始。随着团队对此感到满意,他们希望开始创建更多测试。无论如何,进行一些测试总比没有测试好。

答案 6 :(得分:1)

首先,尝试根据参考书(例如Beck的书)查看您的方法。当你正在学习某些东西时 - 遵循规则而不会质疑。在不了解变更影响的情况下过早适应方法的常见错误 例如(正如Carl所说)我读过的书主张一次编写一个单元测试并在填写实现之前观察它失败。

一旦通过,你需要“重构”。小词,但意义重大 - 它是成败的一步。您可以通过一系列小步骤改进您的设计。然而,TDD无法取代经验......这源于实践。因此,有TDD和/或没有TDD的有经验的程序员可能仍然会产生比TDD新手更好的代码 - 因为他/她知道要注意什么。那你怎么去那里?你向那些已经做了一段时间的人学习。

  • 我首先推荐Beck的TDD By Example书。 (Freeman和Pryce的GOOS book很好,但是一旦你做了TDD一段时间,你会从中获得更好的价值。)
  • 要了解Guru的想法,请查看Bob Martin的Clean Code。它为您提供简单的启发式方法来评估您的选择。我在第3章;我甚至设置了小组阅读练习@ work。正如书中所说,清洁代码是纪律,技术+“代码感”的平等衡量标准。