TDD为什么起作用?[关闭]


92

如今,测试驱动开发(TDD)规模很大。我经常在Programmers SE和其他场所将它推荐为解决各种问题的解决方案。我不知道为什么行得通。

从工程角度来看,这使我感到困惑,原因有两个:

  1. “编写测试+重构直到通过”方法看起来令人难以置信的反工程。例如,如果土木工程师使用该方法进行桥梁建造,或使用汽车设计师作为汽车制造商,则他们将以很高的成本重塑桥梁或汽车,结果将是没有经过深思熟虑的体系结构而造成的混乱。“重构直到通过”准则通常被视为忘记建筑设计并做任何必要的工作以符合测试的要求。换句话说,测试而不是用户来设置需求。在这种情况下,我们如何保证结果中的良好“缺陷”,即最终结果不仅是正确的,而且是可扩展的,健壮的,易于使用的,可靠的,安全的,安全的等。这就是架构通常要做的。
  2. 测试不能保证系统正常运行。它只能表明它没有。换句话说,测试可能会向您显示如果系统未通过测试,则该系统包含缺陷,但是通过所有测试的系统并不比未通过测试的系统安全。测试覆盖率,测试质量和其他因素在这里至关重要。在民用和航空航天业中,已经报道了“绿色”结果给许多人带来的错误安全感觉,这是极其危险的,因为它可能被解释为“系统还不错”,而当它真正意味着“系统还不错时”。作为我们的测试策略”。通常,不检查测试策略。或者,谁来测试测试?

总而言之,我更关心TDD中的“驱动”位而不是“测试”位。测试完全可以;我没有得到的是通过设计来驱动设计。

我想看到一些答案,这些答案包含了为什么软件工程中的TDD是一种好的做法以及为什么我上面解释的问题与软件无关(或不足够相关)的原因。谢谢。


53
桥梁,汽车和其他物理设计远不及软件那样具有延展性。这是一个重要的区别,这意味着软件与实际工程之间的比较并不总是很重要。适用于网桥的软件可能不适用于软件,反之亦然。
拉尔斯·维尔曾尼厄斯

9
我有点同意你的怀疑。例如,我承认我的印象是,拥有测试套件可以在编写代码时以某种方式“软化”注意力。当然,测试是一件好事(如果您希望有重构的可能性,则必须进行测试),但是只有当它们补充了对细节,边界案例,效率或可扩展性的关注时,而不是替代它们时,才是好的。
6502

2
@ 6502:一定!TDD并非灵丹妙药,也无法解决软件开发过程中出现的所有问题。但是,这是组织工作流程的有用方法。例如,您可以强制要求所有边界案例都包含在测试中。您仍然需要知道这些边界情况是什么,但是现在您还拥有一个工具来检查您的代码是否正确处理了它们。
Mchl 2011年

2
@CesarGon,您在我前段时间在SO上问过的这个问题中也可能很有趣……不是TDD,而是相关的……那里有一些非常启发性的答案。
AviD 2011年

6
没有一个土木工程/软件开发的模拟如此令人惊讶。同样,我经常注意到,我不能像修剪草坪一样烹饪煎饼。

Answers:


66

我认为这里有一个误解。在软件设计中,设计与产品非常接近。在土木工程,建筑设计中,设计与实际产品是分离的:存在保留设计的蓝图,然后将这些蓝图具体化为最终产品,并且将这些蓝图与大量的时间和精力分开。

TDD正在测试设计。但是,每个汽车设计和建筑设计也都经过测试。首先计算建筑技术,然后以较小规模进行测试,然后以较大规模进行测试,然后再将其发布到实际建筑物中。例如,当他们发明H型梁和负载时,请放心,在实际使用它建造第一个桥之前,已经对其进行了尝试和尝试。

还可以通过设计原型来测试汽车的设计,是的,当然可以通过调整不完全正确的方法来进行测试,直到达到期望。但是,此过程的一部分速度较慢,因为如您所说,您对产品的了解不多。但是,每一辆汽车的重新设计都借鉴了以前的经验,每座建筑物在空间,光线,绝缘,强度等方面的重要性背后都有数千年的基础。建筑物中的细节都在不断变化和改进。并针对新版本进行重新设计。

此外,还对零件进行了测试。也许与软件的样式并不完全相同,但是通常会测量机械零件(车轮,点火器,电缆)并施加压力,以了解尺寸是否正确,是否有异常现象等等。它们可能会被X射线或激光照射,测量时,他们会敲击砖块来发现破损的砖块,可能会在某种配置下对它们进行实际测试,或者对大型组进行有限的表示以将其真正用于测试。

这些就是您可以使用TDD放置的所有内容。

确实,测试不能保证。程序崩溃,汽车抛锚,当风吹来时,建筑物开始做有趣的事情。但是...“安全性”不是布尔值问题。即使您无法涵盖所有​​内容,也可以覆盖-例如-99%的可能性比仅覆盖50%的可能性要好。如果不进行测试,然后发现钢并没有很好地沉降,那么它会变脆并且在刚放上主要结构时会在第一次敲击时断裂,这简直就是浪费金钱。还有其他问题可能仍会伤害建筑物,但这并不能使它变得如此愚蠢,以至于可以轻易预防的缺陷使您的设计下降。

对于TDD的实践,这是一个平衡问题。以一种方式进行操作的成本(例如,不进行测试,然后再整理零件),而以另一种方式进行操作的成本。它始终是一种平衡。但是不要认为其他设计过程没有适当的测试和TDD。


7
+1讨论制造中在哪里进行测试。好点。
亚当李尔

11
您说“零件已测试”。可以,但不是测试驱动的设计。飞机零件的设计不是以测试驱动的方式进行,而是以结构化,前期设计的方式进行。与TDD的相似性在这里不存在。
CesarGon 2011年

3
另外,我认为TDD主要涉及确保您可以检查零件的方法,而不是最后检查大的“全有或全无”的方法。但是,TDD的“首先建立测试”的提法并不意味着“在考虑要完成的事情之前先进行测试”。因为测试是设计的一部分。在设计中要指定您想要该确切部分执行的操作。在开始键入之前,您已经进行了一些设计。(通过这种方式,我认为“测试驱动设计”一词会误导性表示单向路径,实际上这是一个反馈回路)。
印加

2
+1:软件纯粹是设计。问题中的桥梁类比是完全错误的。TDD完全适用于外部单元测试。测试驱动设计适用于设计的所有层。
S.Lott

3
@CesarGon:不,TDD正在通过测试推动开发。这与驱动设计不同。设计决定了您将如何使用系统,从而决定了需要执行哪些测试来复制该行为。不过,实施这些测试通常可以帮助您优化设计。
deworde 2011年

26

IMO,TDD的大多数成功案例都是伪造的,只是出于营销目的。它可能很少成功,但仅适用于小型应用程序。我正在使用TDD原理的大型Silverlight应用程序上工作。该应用程序已进行了数百次测试,但仍不稳定。由于复杂的用户交互,应用程序的某些部分无法测试。产生的测试带有大量的模拟和难以理解的代码。

最初,当我们尝试TDD时,一切似乎都很好。我能够编写许多测试,并模拟出难以进行单元测试的部分。一旦有了足够多的代码并且需要更改接口,您就会被搞砸。需要修复许多测试,并且您将重写比实际更改更多的测试。

彼得·诺维格(Peter Norvig)在《编码员在工作》一书中解释了他对TDD的看法。

Seibel:使用测试驱动设计的想法怎么样?

Norvig:我将测试更多地看作是纠正错误的一种方式,而不是一种设计方式。这种极端的说法是:“首先,您要做的是编写一个测试,说最后我得到了正确的答案”,然后运行它,发现它失败了,然后您说:“我该怎么办?还需要下一步吗?”,这似乎不是为我设计东西的正确方法。看起来,只有如此简单以至于预先确定解决方案才有意义。我认为您必须首先考虑它。您必须说:“碎片是什么?在知道了其中的一些内容之后,我该如何为它们编写测试?”然后,一旦您完成了,那么对每个作品进行测试并很好地理解它们之间的相互作用是很好的纪律。以及边界情况等等。这些都应该进行测试。但是我不认为您会说“此测试失败了”。


7
现在,如果您将这些事实告诉TDD的人员和顾问,您将得到的答案是well, you haven't done TDD right!
Navaneeth KN11年

10
而且他们会是对的。我们正在非常高容量的系统上执行BDD / TDD,并且运行良好。测试在那里告诉您您违反了预期的行为。如果您打算在以后“破坏”测试并进行更改,则实际上您做错了。应该首先更改测试以确保系统的新行为,然后再进行更改。是的,如果您做得正确,则可以从“此功能需要做什么”开始编写测试,编写测试的过程可以帮助您认为“ IT部门需要做什么”。哦,从来没有使用过顾问...
Andy

4
进行大量测试并不能免除您创建适当设计的麻烦。高度耦合的设计,不管围绕它进行多少测试,都将是脆弱的。在这种设计中嵌入测试可能会使整个情况变得更糟。
Newtopian 2011年

3
这不是做错或成为高度耦合的设计的问题。事实是接口发生了变化。这意味着所有使用该接口的测试都必须更改。在大型系统上,使测试与所需的更改保持同步可能会开始使实施不堪重负。如果您要进行敏捷开发,这将成为更大的问题,因为接口更改的可能性更大。有趣的是,当方法论不起作用时,方法论的支持者坚持认为您做错了。该方法更有可能不适用于所有问题领域。
邓肯

2
以我的经验,TDD适用于小型应用程序或模块。当我不得不处理一些复杂的TDD时,它会使我放慢速度,因为这迫使我在无法清楚了解整体情况之前就编写了详细的(可运行的)规范:因此,我太早地迷失了细节,常常不得不如果发现不需要某些类(我仍在使用设计),则放弃大量测试。在这种情况下,我宁愿先获得合理的总体设计,然后充实实现细节(可能使用TDD)。
乔治

25

测试驱动设计对我有用,原因如下:

它是规范的可运行形式。

这意味着您可以从测试用例中看到:

  1. THAT被调用的代码是否按照预期结果是正确的,在测试用例全填满规范。目视检查(期望测试用例通过)可以立即说“哦,该测试检查给定发票invoiceCompany的这种情况,应该有那个结果”。
  2. HOW代码应该叫。直接指定执行测试所需的实际步骤,而无需任何外部支架(模拟数据库等)。

您首先从外部编写视图。

编写代码的方式通常是解决问题,然后考虑如何调用刚刚编写的代码。这通常会给用户带来尴尬的界面,因为通常更容易“仅添加标志”等。考虑到“我们需要这样做,使测试用例看起来像那样”,就可以解决这个问题。这将提供更好的模块化,因为将根据调用接口而不是相反的方式编写代码。

通常,这也将导致代码更简洁,从而需要较少的解释性文档。

你做得更快

由于您具有可运行形式的规范,因此在完整的测试套件通过后就可以完成。在更详细地阐明事物时,您可以添加更多测试,但是作为基本原理,您将获得非常清晰可见的进度指示器以及完成时间。

这意味着您可以告诉您什么时候有必要做某事(它有助于通过测试),最终您需要做的事少了。

对于那些对它可能有用的人,我鼓励您在下一个库例程中使用TDD。慢慢建立可运行的规范,并使代码通过测试。完成后,所有需要了解如何调用库的人都可以使用可运行的规范。

最近的研究

“案例研究的结果表明,相对于未使用TDD做法的类似项目,这四种产品的预发布缺陷密度降低了40%至90%。从主观上讲,团队的经验表明,这些产品的发布前缺陷密度增加了15%至35%。采用TDD之后的初始开发时间。” 〜成果和经验4个工业队


5
我要补充一点,您实际上对完成工作有一些合理而清晰的指导。如果没有一些清晰的过程来客观地验证您已完成手头的任务,那就很难了。我自己的经验包括浪费大量时间和精力“协商”任务是否已完成以及生产线持续不断地移动。这会影响项目管理的所有级别,包括计划,因为您如何计划这样的任务?更清晰的目标和更快的周转时间可提高吞吐量和沟通。
Edward Strange

这应该是公认的答案。
宁宁

19

创建软件的过程不是编写代码的过程。没有“广泛的范围”计划,任何软件项目都不应首先开始。就像跨河两岸的项目一样,首先需要这样的计划。

TDD方法(主要)与单元测试有关-至少这是人们倾向于考虑的方式-这是创建软件代码的最低级位。当已经定义了所有功能和行为并且我们实际上知道我们想要实现的目标时。

在结构工程中,它看起来像这样:

``我们将这两块金属连接在一起,并且连接需要承受x数量级的剪切力。让我们测试哪种连接方法是执行此操作的最佳方法。

为了测试软件是否可以整体运行,我们设计了其他类型的测试,例如可用性测试,集成测试和验收测试。这些也应该在编写代码的实际工作开始之前定义,并在单元测试为绿色之后执行。

请参见V模型:http//en.wikipedia.org/wiki/V-Model_%28software_development%29

让我们看看它如何在桥梁上工作:

  1. 地方政府对一家桥梁建筑公司说:“我们需要一座桥梁来连接这两个点。桥梁需要每小时能够允许n的交通量,并且要在2012年12月21日之前准备就绪”-这是对验收测试:如果公司无法通过该测试,则公司将不会获得全额(或任何)款项。

  2. 公司的管理决定项目进度。他们建立工作团队并为每个团队设定目标。如果团队无法实现这些目标-桥梁将无法按时建成。但是,这里有一定程度的灵活性。如果其中一个团队遇到问题,公司可以通过更改需求,更改分包商,雇用更多人员等来弥补这一点,从而使整个项目仍达到第1点中设定的目标。

  3. 在负责设计特定桥梁组件的团队中,就像我在上面的示例中所示。有时解决方案很明显,因为我们拥有大量有关搭建桥梁的知识(就像在软件开发中使用经过良好测试的库一样,您只是假设它按宣传的方式工作)。有时您需要创建多个设计并对其进行测试以选择最佳设计。仍然要事先知道测试组件的标准。


如果我对您的理解是正确的,那么您是说TDD可以,只要(a)仅用于单元测试,并且(b)还附带其他测试方法即可。在这种情况下,它可以寻址OP中的第2点。您将如何处理第1点?
CesarGon 2011年

@CesarGon:TDD在集成测试中也非常有用。
sevenseacat 2011年

要点1归结为该法案,即在汽车或桥梁的最终项目被接受之前,它要经过多次重复,在此期间,所有细节都要根据“广泛计划”的要求进行审查和测试。它主要是在纸上(或在计算机内存中)完成的,因为在这种情况下更便宜,但请注意,通常有物理原型正在构建整个结构(也许不是在桥的情况下)及其组件。
Mchl 2011年

@Karpie:还要进行验收测试!您应该事先知道要让客户接受您的工作需要什么。
Mchl 2011年

1
好吧。首先要开始工作的团队是建筑师团队,他们被要求设计一种能够满足客户标准的桥梁,同时价格便宜,看上去也不错,并且不会在第一阵风中倒下。团队可能会提出一些或多或少满足这些标准的粗略设计,然后选择一个并进行更详细的处理,重复,重复,重复直到设计就绪(即满足给定的标准并且足够详细,以便项目的其他阶段可以开始)
Mchl 2011年

18

在我看来,TDD之所以有效,是因为

  • 它会迫使您在确定实现之前,以某种规格或要求文档通常未涵盖的精确度来定义您希望单位执行的操作
  • 它使您的代码具有固有的可重用性,因为您必须在测试和生产场景中都使用它
  • 它鼓励您以较小的难度编写代码来测试块,这往往会导致更好的设计

特别是你提出的要点

  • 代码比砖或钢更具延展性,因此修改便宜。如果您进行测试以确保行为不变,则价格仍然便宜
  • TDD并非不进行设计的借口-仍然建议使用高级架构,只是不要过多地进行细节设计。不鼓励进行Big Up Front设计,但鼓励进行足够的设计
  • TDD不能保证系统能正常工作,但是它可以防止许多小错误,否则这些小错误就会被遗漏。另外,由于它通常鼓励使用更好的分解代码,因此通常更易于理解,因此不太可能出现错误

3
您还应该补充一点,当发现缺陷时,可以确保不会重复出现缺陷,因为您将添加另一个测试。
安迪

16

TL; DR

编程仍然是设计活动,不是构造。在事实发生之后编写单元测试只能确认该代码已完成其工作,而不是确定它已完成了一些有用的工作。测试失败是真正的价值,因为它们可以让您及早发现错误。

代码就是设计

PPP的第7章中,“鲍勃叔叔”直接讨论了这个问题。在本章的开始,他引用了Jack Reeves的一篇出色的文章,他在文章中建议代码是设计的(链接指向收集该主题的所有三篇文章的页面)。

关于这一论点的有趣之处在于,他指出,与其他工程学科的建设活动非常昂贵相比,其他工程学科相对而言,软件的构建相对来说是免费的(在您的IDE中编译并拥有已构建的软件)。如果将编写代码视为设计活动而不是构造活动,那么红绿重构循环基本上就是设计中的练习。设计会随着编写测试,满足测试要求的代码以及重构以将新代码集成到现有系统中而发展。

TDD作为规范

您理解为TDD编写的单元测试是对规范的直接翻译。通过编写最低限度满足您的规范的代码(使测试变成绿色),您编写的所有代码都将用于特定目的。通过重复测试可以验证该目的是否得到满足。

编写测试功能

单元测试中的一个常见错误是,您在代码之后编写测试,最终测试该代码执行的工作。换句话说,您将看到这样的测试

public class PersonTest:Test
{
   [Test]
   TestNameProperty()
   {
      var person=new Person();
      person.Name="John Doe";
      Assert.AreEqual("John Doe", person.Name);
   }
}

虽然我认为这段代码可能很有用(请确保某人没有通过简单的属性进行淫秽的操作)。它不能用来验证规范。正如您所说,编写此类测试只会带您走那么远。

当绿色是好的,价值在于红色 时,当我遇到意外的测试失败时,我在TDD中有了第一个真正的“ aha”时刻。我有一套针对正在构建的框架的测试。添加一个新功能后,我为此进行了测试。然后编写代码以使测试通过。编译,测试...在新测试上获得绿色。但是在另一个我没想到会变红的测试中也变红了。

看着失败,我松了一口气,因为如果我没有适当的测试,我怀疑我会在相当长的一段时间内发现该错误。这是一个非常讨厌的错误。幸运的是,我进行了测试,它准确地告诉了我修复该错误所需的操作。没有测试,我将继续构建我的系统(该错误会感染依赖于该代码的其他模块),并且在发现该错误时,正确修复它将是一项主要任务。

TDD的真正好处在于,它使我们能够毫不留情地进行更改。就像编程的安全网一样。想想如果空中飞人失误跌倒会发生什么。使用网络,这是一个令人尴尬的错误。没有,这是一个悲剧。同样,TDD可以使您避免将愚蠢的错误变成项目致命的灾难。


4
捕获错误的红色测试的值通常是单元测试的属性,而不是TDD的特定属性。
罗伯特·哈维

2
您说得对。但是,在事后单元测试中涵盖该特定错误的可能性更低。
迈克尔·布朗

1
您可以提供一些证据,数据或可靠的分析来支持该主张吗?
CesarGon 2011年

1
@CesarGon的这项研究是针对从事小型项目的程序员的,这表明使用TDD的开发人员所生成的代码具有比事后进行测试的代码更好的测试覆盖率(92%-98%对80%-90%),因此可以捕获更多代码开发过程中的缺陷(使用TDD生成的代码中发现的缺陷减少了18%)。
2014年

11

您不会找到任何人主张测试驱动开发,甚至提倡测试驱动设计(它们是不同的),它们说测试证明了应用程序。因此,让我们称其为稻草人即可。

您不会发现任何不喜欢TDD或没有对TDD印象深刻的人,它说测试是浪费时间和精力。尽管测试不能证明应用程序,但是它们对于发现错误很有帮助。

说了这两点,对于在软件上实际执行测试,双方都没有做任何不同的事情。两者都在做测试。两者都依靠测试来发现尽可能多的错误,并且都使用测试来验证软件程序是否正常运行以及是否可以在当时被发现。没有半点头绪的人会在没有测试的情况下出售软件,没有半点头绪的人不会期望测试将使他们出售的代码完全没有错误。

因此,TDD和not-TDD之间的区别不是正在进行测试。区别在于编写测试的时间。在TDD中,测试是在软件之前编写的。在非TDD中,测试是在软件之后或与软件配合进行的。

我所看到的关于后者的问题是,测试然后倾向于将目标对准要编写的软件,而不是期望的结果或规范。即使测试团队与开发团队是分开的,测试团队也倾向于查看软件,使用软件并编写针对软件的测试。

研究项目成功的人一次又一次注意到的一件事是,客户多久会提出他们想要的东西,开发人员跑出来写点东西,并且当他们回到客户那里说“完成”时事实证明,这完全不是客户要求的。“但是它通过了所有的测试……”

TDD的目标是打破这种“循环论证”,并为测试不是软件本身的软件的测试提供基础。编写测试以针对“客户”想要的行为。然后编写该软件以通过那些测试。

TDD是旨在解决此问题的解决方案的一部分。这不是您迈出的唯一一步。您需要做的其他事情是确保有更多的客户反馈并且更频繁。

但是以我的经验来看,TDD很难成功实施。在产品推出之前很难编写测试,因为许多自动化测试都需要进行一些操作才能使自动化软件正常工作。要使不习惯单元测试的开发人员也很难做到这一点。我一次又一次地告诉团队中的人首先编写测试。我从来没有真正做到这一点。最后,时间限制和政治破坏了所有努力,因此我们甚至不再进行单元测试。当然,这不可避免地导致了设计的偶然性和严重的耦合,因此即使我们愿意,现在实施起来也代价过高。TDD最终为开发人员提供了避免THAT。


+1感谢您的全面答复,诺亚。我同意,TDD和非TDD之间的主要区别在于编写测试的时间。但是,我也认为TDD中的第一个“ D”代表“驱动”,这意味着,在TDD中,整个开发过程都是由测试驱动的。这是我最困惑的地方。在实际构建要测试的内容之前,我在编写测试方面没有任何问题。但是让测试驱动吗?只要表面(即结果)还可以,做什么与绿灯有什么不同?
CesarGon 2011年

好吧,塞萨尔,您将提出什么更好的客观标准来决定何时完成开发任务?如果像TDD中那样,测试是开发人员针对的规范,那么开发人员在测试通过时就完成了工作,不是吗?是的,测试中可能存在缺陷,就像任何规范中都可能存在缺陷一样。不过,这不是开发人员要解决的任务。如果测试有缺陷,那么它将得到修复,然后开发将目标对准新目标,并且当所有目标都变为绿色时,就可以完成。之所以有效,是因为总有一项测试可以通过...没有多余的,没有证件的绒毛。
爱德华·斯特兰奇

3
也许我没有表达清楚自己。测试可能是确定何时完成的好方法。但是我认为它们不是决定必须构建的内容的好方法。而且,在TDD中,我发现人们正在使用测试来决定他们应该构建的东西。那也是你的经历吗?
CesarGon 2011年

否。我们的构建是自动化的。它们是由变化触发的。就像我说的那样,TDD只是解决方案的一部分。
Edward Strange

9

首先设计

TDD 不是跳过设计的借口。我已经看到许多“敏捷”潮流中的飞跃,因为它们虽然可以立即开始编码。真正的敏捷将使您进行状态编码的速度快于启发瀑布过程的(其他领域)工程良好实践。

但是尽早测试

当人们说测试正在推动设计时,它仅表示人们可以在设计阶段的很早就使用测试,而这要早于完成。进行此测试将对灰色区域提出挑战,并在产品完成之前很久就将其与现实世界进行对比,从而对您的设计产生重大影响。迫使您经常回到设计并对其进行调整以考虑到这一点。

测试和设计...一模一样

在我看来,TDD只是将测试作为设计的组成部分,而不是最后进行验证。随着您开始越来越多地使用TDD,您会在设计时就着手解决如何破坏/破坏系统的想法。我个人并不总是先进行测试。当然,我在接口上进行了显而易见的(单元)测试,但是真正的收获来自于我想到这种设计可以打破的新颖且创造性的方式时所创建的集成和规范测试。我一想到一种方法,便为它编写了一个测试代码,然后看看会发生什么。有时我可以忍受这种后果,在这种情况下,我将测试移到一个不属于主构建的单独项目中(因为它将继续失败)。

那谁来主持演出呢?

在TDD中,此处的驱动只是意味着您的测试对设计的影响如此之大,以至于您可以感觉到他们实际上在驱动它。但是,到此为止,我明白您的担忧,这有点吓人...谁来主持这场演出?

正在驾驶,而不是测试。测试在那里进行,因此随着您的前进,您对所创建的内容会充满信心,从而使您进一步了解它基于扎实的基础。

只要测试是可靠的就可以

确实如此,因此驱动了TDD。测试并不是驱动整个事情的重要因素,但是它们将对您的工作方式,设计和思维方式产生深远的影响,因此您会将大部分思维过程委托给测试,并以此作为回报。它们将对您的设计产生深远的影响。

是的,但是如果我用我的桥来做...

停在这里...软件工程与那里的其他工程实践完全不同。实际上,软件工程实际上与文献有很多共同点。一个人可以读一本完整的书,从中撕掉4章,然后写两个新的章来代替它们,将它们重新粘贴在书中,您仍然会拥有一本好书。有了良好的测试和软件,您可以撕裂系统的任何部分并用另一部分替换,这样做的成本并不比它最初创建时要高得多。实际上,如果您进行了测试并允许它们充分影响您的设计,则它可能比最初创建它会便宜得多,因为您将有一定程度的信心,认为这种替代不会破坏测试涵盖的范围。

如果它太好了,怎么不总是起作用?

因为测试与构建所需要的思维方式非常不同。并非每个人都能来回切换,实际上,有些人将无法仅仅因为无法下定决心破坏自己的创作而建立适当的测试。这将导致项目进行的测试太少或测试不足以达到目标指标(想到代码覆盖率)。他们会乐于进行路径测试和异常测试,但会忘记极端情况和边界条件。

其他人将仅依靠部分或全部放弃测试的测试。每个成员这样做都是一件事情,然后彼此整合。设计首先是一种沟通工具,我们在地上树立的标语是我将要去的地方,草图则是这是门窗将要去的地方。没有此功能,无论您进行了多少测试,您的软件都将注定失败。集成和合并将一直很痛苦,并且它们将缺少最高抽象级别的测试。

这些团队TDD可能并不是要走的路。


7

使用TDD时,您往往不会编写测试起来不容易或不快速的代码。这看起来似乎是一件小事,但它可能会对项目产生深远的影响,因为它会影响重构,测试,重现测试错误和验证修复的难易程度。

当您具有更好的测试支持的代码时,项目中的新开发人员也可以更快地上手。


2
我喜欢这一点-它强调的一点是,它产生的好处不是很多TDD(尽管显然进行单元测试具有巨大的价值),而不是它产生的那种可测试的(独立的)代码。随之而来的是各种各样的好事(关注点分离,IoC和依赖注入等)
Murph

1
@Murph yeah TDD可以帮助您保持诚实:)
Alb

1
老实说,我实际上并不相信“更容易上手”的说法-测试可能会有所帮助,但是代码(整体上,不一定是孤立的)可能更难解码,因为有些东西会看起来好像是魔术般的,例如,您不知道正在使用IInjectedThing的哪种实现。
Murph

@Murph从理论上讲,实现IInjectedThing的方式也设计得很好,并包含良好的测试,因此,您真正不需要了解它的含义就可以理解被注入的类。
亚当李尔

@Anna-是的,在某种程度上……如果您要尝试找出出现问题的地方(我总是觉得寻找bug是找到立足于项目的好方法)或需要更改的地方/添加您需要知道在哪里。即使封装得很好,您仍然需要找到它...并且如果它意味着替换某些东西(IWhatsit的新实现),则您需要知道如何使用替代实现。再一次,我并不是在谴责这种结构是不好的-相反的证据太多-而是暗示某些事情可能不太明显。
Murph

5

尽管我自己没有那么多地练习TDD,但我一直在思考很多。代码质量与后续TDD之间似乎存在(强?)正相关。

1)我的初衷是,这(主要)不是因为TDD在代码中(因此)添加了“更好的质量”,这更像是TDD帮助清除最差的部分和习惯,从而间接地提高了质量。

我什至主张,这不是测试本身,而是编写这些测试的过程。很难为错误的代码编写测试,反之亦然。并在编程时将其放在首位,消除了许多不良代码。

2)另一种观点(这是哲学上的)是遵循主人的心理习惯。您不会通过遵循他的“外部习惯”来学习成为大师(例如,留胡子的习惯是好的),您必须学习他的内部思维方式,这很难。使(新手)程序员遵循TDD,使他们的思维方式更接近于大师。


+1我认为您已将其钉牢,Maglob。我特别喜欢您的解释,即“ TDD有助于清除最差的部分和习惯,从而间接提高质量”。长长的胡须比喻也很好。
CesarGon 2011年

您不会为错误的代码编写测试,而是先编写测试,然后编写代码以使测试通过。

Maglob,对事物更实际的一面的热爱,使您获得了最好的覆盖。@Thorbjørn,我认为Maglob沿线走得更远,如果您的预期设计糟透了,您的测试肯定会直接吸收到您想要实现的糟透程度,并且臭味应该在您的测试之前出现您甚至可以编写任何实际的代码。
FilipDupanović2011年

3

“编写测试+重构直到通过”方法看起来令人难以置信的反工程。

您似乎对重构和TDD都有误解。

代码重构是更改计算机程序源代码而不修改其外部功能行为,以改善软件的某些非功能属性的过程。

因此,直到代码通过,您才能重构代码。

TDD,特别是单元测试(我认为是核心的改进,因为其他测试对我来说似乎很合理),并不是在重新设计组件之前就可以进行工作。它是关于设计组件并进行实现直到组件按设计工作为止。

真正掌握单元测试与测试单元有关也很重要。由于总是从头开始编写很多东西的趋势,因此测试此类单元很重要。一位土木工程师已经知道他使用的单元的规格(不同的材料),并且可以期望它们能够工作。这是两件事,通常不适用于软件工程师,并且在使用它们之前非常容易进行工程设计,因为这意味着要使用经过测试的高质量组件。
如果土木工程师有想法使用一些新的纤维纸来制作覆盖体育场的屋顶,则您希望他将其作为一个单元进行测试,即定义所需的规格(例如重量,渗透性,稳定性等),并然后进行测试和优化,直到满足要求。

这就是TDD起作用的原因。因为如果您构建经过测试的单元的软件,则将其工作的机会要大得多,将它们连接在一起时,如果没有,则可以认为问题出在您的粘合代码中,前提是您的测试覆盖范围广。

编辑:
重构意味着:功能没有改变。编写单元测试的一点是要确保重构不会破坏代码。因此,TDD的目的是确保重构不会产生副作用。
粒度不是透视图的主题,因为正如我所说,单元测试是测试单元而不是系统,因此可以精确定义粒度。

TDD鼓励良好的体系结构。它要求您为所有单元定义和实施规格,迫使您在实施之前设计它们,这与您似乎想的完全相反。TDD规定了单元的创建,这些单元可以单独测试,因此可以完全解耦。
TDD并不意味着我对意大利面条代码进行了软件测试,并搅拌通心粉直到通行。

与土木工程不同,在软件工程中,项目通常会不断发展。在土木工程中,您需要在位置A建造一座桥梁,该桥梁可以承载x吨,并且足够宽,每小时可容纳n辆汽车。
在软件工程中,客户基本上可以在任何时候决定(可能在完成后),他想要一个双层桥,并且希望它与最近的高速公路连接,并且他希望它成为吊桥,因为他的公司最近开始使用帆船。
软件工程师的任务是更改设计。不是因为他们的设计有缺陷,而是因为那是作案手法。如果软件设计良好,则可以在不重新编写所有底层组件的情况下,从高层次进行重新设计。

TDD是关于使用经过单独测试的,高度分离的组件来构建软件的。如果执行得当,它将比不使用它更快,更安全地帮助您响应需求的变化。

TDD在开发过程中增加了要求,但并不禁止任何其他质量保证方法。诚然,TDD不能提供与形式验证相同的安全性,但是同样,形式验证非常昂贵,并且无法在系统级别使用。而且,如果您愿意,可以将两者结合起来。

TDD还包含在系统级别执行的单元测试以外的测试。我发现这些内容很容易解释,但难以执行且难以衡量。而且,它们很合理。尽管我绝对看到它们的必要性,但我并没有真正将它们视为思想。

最后,没有工具能够真正解决问题。工具只会使解决问题更加容易。您可以问:凿子将如何帮助我打造出色的建筑?好吧,如果您打算做直墙,直砖会有所帮助。是的,当然,如果您将该工具提供给白痴,他可能最终会用脚猛击它,但这不是凿子的错,这并不是TDD的缺陷,它给新手提供了虚假的安全保障,谁没有写好的测试。
因此,最重要的是,可以说TDD比没有TDD的要好得多。


我不认为我有误解。我同意您发布的代码重构的定义,但我也认为您需要查看代码更改的粒度。当您说“ 更改计算机程序源代码的过程”时,您需要认识到,从某个整体的角度来看,其行为没有改变,但是各部分的行为确实发生了变化。这就是改变发生的方式。除此之外,我还听您介绍了TDD为何起作用(并与我分享),但是按照我的原始帖子,如何解决体系结构?
CesarGon 2011年

@CesarGon:帖子已更新。
back2dos

2

我不喜欢您说“测试而不是用户来确定要求”。我认为您只考虑在TDD中进行单元测试,而它也涵盖了集成测试。

除了测试构成软件基础的库之外,还要编写覆盖用户与软件/网站/所有内容的交互的测试。这些直接来自用户,像Cucumber(http://cukes.info)之类的库甚至可以让您的用户以自然语言自己编写测试。

TDD还鼓励代码的灵活性-如果您花很多时间设计某些东西的体系结构,那么以后在必要时进行那些更改将非常困难。首先编写一些测试,然后编写一些通过这些测试的代码。添加更多测试,添加更多代码。如果需要从根本上更改代码,则测试仍然有效。

不像桥梁和汽车,一个单件的软件可以在其生命周期发生了巨大的变化,和做复杂的重构,而不必首先编写了测试只是了麻烦。


我听说您要求TDD带来好处。但是据我了解,您没有解决我在问题中明确要求的体系结构和测试质量问题。
CesarGon 2011年

@CesarGon:我认为您的特定问题适用于任何类型的测试,而不仅仅是TDD。因此,我只关注“工作”的TDD的特定功能。
sevenseacat 2011年

1
集成测试绝对比独立单元测试更有意义。大多数错误情况下,我偶然发现根本就不会被单位测试中发现,只有通过测试的整个现实与所有的螺栓和口哨声系统到位。

2

我认为您正在从错误的角度接近第一个问题。

从理论的角度来看,我们证明可以通过检查故障点来解决问题。那就是使用的方法。您可能有许多其他方法可以证明某项功能正常,但是TDD的建立是由于其按位方式的简单性:如果不破坏,它将起作用。

实际上,这直言不讳地转化为:我们现在可以继续进行下一件事情(在成功应用TDD满足所有谓词之后)。如果您从这种角度看待TDD,那么这与“编写测试+重构直到通过”无关,而更多的是要完成此操作,我现在将重点放在下一个功能上,这是目前最重要的事情

考虑一下这如何适用于土木工程。我们正在建设一个体育场,可容纳15万人。在证明体育场的结构完整性良好之后,我们首先满足了安全性要求。现在,我们可以集中精力处理其他紧迫重要的问题,例如洗手间,食品摊位,座位等……使听众的体验更加愉悦。这是一个过分的简化,因为TDD还有很多其他功能,但症结在于,如果您同时专注于令人兴奋的新功能并保持完整性,那么您将无法获得最佳的用户体验。在两种情况下,您都会获得成功。我的意思是,你怎么能确切知道许多洗手间,您应该在哪里放置15万人?我很少见到体育场在我的一生中倒塌,但是我不得不在很多情况下半场排队等候。那就是说洗手间问题可以说更复杂,如果工程师可以花更少的时间在安全性上,他们也许最终能够解决洗手间问题。

您的第二点无关紧要,因为我们已经同意绝对是愚蠢的努力,并且因为汉克·穆迪(Hank Moody)表示绝对不存在(但我似乎找不到相应的参考)。


+1可以很好地解释我的第一点,也可以参考Hank Moody。辉煌。
CesarGon 2011年

2
谢谢,我很感激。我将TDD视为一种心理现象,而不是一种技术方法/过程。但这只是我对此事的世界观。
FilipDupanović2011年

您能确切知道多少个盥洗室以及应将它们放置在何处吗?答案是肯定的-请教任何架构师,他们会告诉您该信息是预先准备好的,有时还会提供清晰的统计数据来进行备份。
gbjbaanb 2013年

1

软件工程中的TDD是一种好习惯,与应用程序中的错误处理以及日志记录和诊断一样(尽管它是错误处理的一部分)也是一种好习惯。

TDD不能用作将软件开发减少为试验和错误编码的工具。但是,大多数程序员仍然在开发阶段盯着运行时日志,观察调试器中的异常情况或使用其他失败/成功的迹象,其中包括整天编码/编译/运行应用程序。

TDD只是使这些步骤正式化和自动化的一种方法,可以使您作为开发人员更加高效。

1)您无法将软件工程与桥梁建设相提并论,桥梁建设的灵活性与设计软件程序的灵活性相去甚远。构造网桥就像将相同的程序一遍又一遍地写入有损机器中。网桥不能像软件那样重复和重用。每个桥都是唯一的,必须制造。汽车和其他设计也是如此。

软件工程中最难的事情是重现故障,当网桥发生故障时,通常很容易确定出了什么问题,从理论上讲,重现故障也很容易。当计算机程序失败时,它可能是一系列复杂的事件,使系统进入错误状态,并且很难确定错误的位置。TDD和单元测试使测试软件组件,库和算法的健壮性更加容易。

2)使用弱单元测试和浅测试用例,它们不会使系统产生虚假的信心,这只是一个坏习惯。当然,忽略系统的体系结构质量并仅仅完成测试也是很糟糕的。但是在建筑工地作弊以节省摩天大楼或桥梁以节省材料而不遵循蓝图是同样糟糕的,而且这种情况一直在发生……


我不同意您的暗示,即在物理(即非软件)系统中很容易重现故障。例如,研究确定航空交通事故中机械故障的根本原因所必需的极其复杂,艰苦的工作。
CesarGon 2011年

嗯,现在您将坠机客机与发生故障的机桥进行比较,通常机桥无法飞行,机壳关闭。但是有时飞机和软件之间的比较是有效的。这两个领域都很复杂,需要结构化的测试方法。因此,当网桥发生故障时,您就知道它已经过载。当飞机坠毁时,您会知道在地面上飞行的异常状态失败了,但是原因通常需要与软件故障一样进行彻底调查。
Ernelli

桥梁可以复制-或者至少,您从建筑师那里购买的桥梁蓝图可以粗略地修改以适合您的实际情况。关键是,如果您需要一座桥梁,您会去找建筑师,他会给您列出您可以拥有的几种类型的清单-悬架,箱形,拱形等,以及建造它的有限材料清单。
gbjbaanb

1

如果您接受发现的bug越早,修复它们的成本越低,那么仅此一项就使TDD值得。


1
您是否有任何证据表明在TDD设置中会更快发现错误?另外,TDD的副作用如何,例如对体系结构的影响?
CesarGon 2011年

0

TDD与测试无关。当然,它不能代替良好的测试。它为您提供的是一种经过深思熟虑的设计,便于消费者使用,并且易于维护和以后重构。这些事情反过来导致更少的错误和更好,更易适应的软件设计。TDD还可以帮助您仔细考虑并记录您的假设,通常会发现其中一些不正确。您会在流程的早期发现这些。

作为一个不错的附带好处,您可以运行大量测试,以确保重构不会改变软件的行为(输入和输出)。


6
-1。很多人一直在说这句话,但是我还没有看到实现它的魔力。
Bart van Ingen Schenau,2011年

@Bart van Ingen Schenau,您完成了TDD吗?我已经做了大约4年了,我肯定已经看到了“魔术”的发生。
玛西(Marcie)

0

我给你一个简短的答案。通常,TDD就像单元测试一样,以错误的方式看待。直到最近,我在观看了精彩的技术讲座视频后才了解单元测试。从本质上讲,TDD只是在说明您希望以下工作。必须实施。然后,您可以按照通常的方式设计其余的软件。

有点像在设计库之前为库编写用例。除了可以在库中更改用例之外,您可能不使用TDD(我将TDD用于API设计)。还鼓励您添加更多测试,并考虑该测试可能会获得的疯狂投入/使用。在编写库或API时,我发现它很有用,如果您更改某些内容,则必须知道自己已破坏某些内容。在大多数日常软件中,我都不会打扰,因为为什么我需要一个供用户按下按钮的测试用例,或者为什么我想接受CSV列表或每行只有一个条目的列表,所以我没有关系。改变它,因此我不应该/不能使用TDD。


0

当结构工程是具体的时,软件是有机的。

当您构建桥梁时,它将继续保持桥梁状态,并且不太可能在短时间内演变为其他事物。改进将持续数月和数年,但不会像软件一样耗时数日。

单独进行测试时,通常可以使用两种类型的框架。受约束的框架和不受约束的。不受限制的框架(在.NET中)使您可以测试和替换所有内容,而无需考虑访问修饰符。即,您可以存根和模拟私有和受保护的组件。

我见过的大多数项目都使用受约束的框架(RhinoMocks,NSubstitute,Moq)。使用这些框架进行测试时,必须以一种可以在运行时注入和替换依赖项的方式设计应用程序。这意味着您必须具有松散耦合的设计。松耦合的设计(正确处理时)意味着更好的关注点分离,这是一件好事。

总而言之,我认为背后的想法是,如果您的设计是可测试的,则它是松散耦合的,并且具有良好的关注点分离。

附带一提,我看到了真正可测试的应用程序,但是从面向对象设计的角度看,这些应用程序编写得很差。


0

TDD为什么起作用?

没有。

澄清:自动化测试总比没有测试好。但是我个人认为,大多数单元测试都是浪费的,因为它们通常是重言式的(即说从实际测试代码中可以明显看出来),并且不能轻易证明它们是一致的,不是多余的并且涵盖所有边界情况(通常会发生错误) )。

最重要的是:优秀的软件设计并没有像许多敏捷/ TDD推广者所宣传的那样神奇地从测试中消失。否则,每个人都请提供指向经过同行评审的科学研究的链接,以证明这一点,或者至少参考一些开源项目,在该项目中,可以通过其代码更改历史来潜在地研究TDD的益处。

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.