如何进行测试驱动开发


15

我在应用程序开发方面只有2年以上的经验。在那两年中,我对发展的态度如下

  1. 分析需求
  2. 身份核心组件/对象,必需功能,行为,过程及其约束
  3. 创建类,它们之间的关系,对象行为和状态的约束
  4. 根据要求创建功能,处理行为约束
  5. 手动测试应用
  6. 如果需求更改修改组件/功能,则手动测试应用程序

最近,我对TDD进行了介绍,并认为这是进行开发的好方法,因为已开发的代码有充分的理由存在,并且可以缓解许多后期部署问题。

但是我的问题是我不能先创建测试,而是要标识组件,并在实际编写组件之前为它们编写测试。我的问题是

  1. 我做对了吗?如果不是,我到底要改变什么
  2. 有什么方法可以确定您编写的测试是否足够?
  3. 是针对非常简单的功能(相当于1 + 1 = 2)编写测试的好习惯吗?
  4. 更改功能并相应地测试需求是否有所变化是否很好?

2
“我正在确定组件,并在实际编写组件之前为它们编写测试。”:我发现这是正确的:首先确定系统的粗略体系结构,然后开始编码。在编码(TDD)期间,您需要计算出各个组件的详细信息,并可能发现体系结构中可以解决的问题。但是我发现没有任何先验分析就不要开始编码是可以的。
Giorgio 2014年

您也可以考虑在进行TDD的情况下进行自动化的单元/集成测试。两者常常是混淆的,但它们不是同一件事。
Andres F.

Answers:


19

我做对了吗?如果不是,我到底要改变什么

很难只从简短的描述说,但我怀疑,不,你这样做是正确的。注意:我并不是说您正在做的事情行不通或在某种程度上是不好的,但是您没有在做TDD。中间的“ D”表示“被驱动”,测试驱动一切,开发过程,代码,设计,体系结构,一切

这些测试告诉您写什么,何时写,接下来写什么,何时停止写。他们告诉您设计和架构。(设计和体系结构是通过重构从代码中产生的。)TDD与测试无关。它甚至不是要先编写测试:TDD是要让测试驱动您,先编写测试只是这样做的必要先决条件。

不管您是实际写下代码还是充分充实代码,这都是无关紧要的:您正在脑海中编写(骨架)代码,然后为该代码编写测试。那不是TDD。

放弃这种习惯很难。真的,真的很难。对于有经验的程序员而言,这似乎特别困难。

Keith Braithwaite创建了一个演习,他将其称为TDD,就好像您是故意的那样。它包含一组您必须严格遵守的规则(基于Bob Martin叔叔的TDD的三个规则,但更为严格),这些规则旨在引导您更加严格地应用TDD。它最适合与结对编程(这样结对可以确保您没有违反规则)和一名讲师一起使用。

规则是:

  1. 准确地编写一个新测试,这似乎是可以解决问题的最小测试
  2. 看到它失败了;编译失败计为失败
  3. 通过编写您可以在test方法中使用的最少实现代码,使(1)通过测试
  4. 重构以消除重复,否则可以根据需要改进设计。严格使用以下举动:
    1. 您需要一个新方法-等待重构时间,然后…通过执行以下方法之一来创建新的(非测试)方法,并且没有其他方法:
      • 首选:对根据(3)创建的实现代码执行Extract Method来在测试类中创建新方法,或者
      • 如果您必须:将(3)中的实现代码移至现有的实现方法中
    2. 您想要一个新类-等待重构时间,然后...创建非测试类以提供Move方法的目的地,而没有其他原因
    3. 通过执行Move方法,用方法填充实现类,而没有其他方法

通常,这将导致与通常使用的“伪TDD方法”截然不同的“伪TDD方法”,即“在脑海中想象设计应该是什么,然后编写测试以迫使该设计,实施您已经设想的设计,然后再编写您的设计测试”。

当一群人使用伪TDD来实现井字游戏之类的东西时,他们通常最终会得到非常相似的设计,涉及某种类型的Board3×3数组Integer。至少有一部分程序员实际上会编写此类而不进行任何测试,因为他们“知道自己将需要它”或“需要一些东西来针对其编写测试”。但是,当您强迫同一个小组按照您的意思来应用TDD时,他们往往最终会得到各种各样的非常不同的设计,通常甚至不使用任何与TDM类似的东西Board

有什么方法可以确定您编写的测试是否足够?

当它们涵盖了所有业务需求时。测试是系统要求的编码。

是针对非常简单的功能(相当于1 + 1 = 2)编写测试的好习惯吗?

再次,您将其倒退:您无需编写功能测试。您编写测试功能。如果通过测试的功能证明是微不足道的,那就太好了!您只满足系统要求,甚至不需要为此而努力!

更改功能并相应地测试需求是否有所变化是否很好?

不,反之亦然。如果需求发生变化,则可以更改与该需求相对应的测试,观察其失败,然后更改代码以使其通过。测试始终是第一位的。

很难做到这一点。您需要数十甚至数百小时的深思熟虑的练习才能建立某种“肌肉记忆”,以便在截止日期迫在眉睫且您承受压力时,甚至不必考虑它,这成为最快,最自然的工作方式。


1
确实是一个非常明确的答案!从实践的角度来看,在实践TDD时非常喜欢灵活而强大的测试框架。尽管与TDD无关,但是自动运行测试的能力对于调试应用程序非常宝贵。要开始使用TDD,非交互程序(UNIX风格)可能是最简单的,因为可以通过将退出状态和程序输出与预期结果进行比较来测试用例。在我的OCaml 汽油库中可以找到这种方法的具体示例。
Michael Le BarbierGrünewald2014年

4
您说:“当您迫使同一小组像您想表达的那样应用TDD时,他们往往最终会获得各种各样非常不同的设计,甚至甚至没有采用与董事会遥遥相似的东西”,这似乎是一件好事。 。对我来说,这还不是很清楚,这是一件好事,从维护的角度来看,甚至可能是坏的,因为听起来这对于新手来说似乎非常违反直觉。您能否解释为什么实现多样性是一件好事,或者至少还不错?
Jim Clay

3
+1答案是好的,因为它可以正确描述TDD。但是,这也说明了TDD为什么是有缺陷的方法:需要仔细思考和明确设计,尤其是在遇到算法问题时。假装没有任何领域知识,“在盲中”(如TDD所规定)进行TDD会导致不必要的困难和死胡同。请参阅臭名昭著的数独求解器崩溃(简短版本:TDD无法击败领域知识)。
Andres F.

1
@AndresF .:实际上,您链接到的博客文章似乎呼应了Keith在进行TDD时所做的体验,就好像您是说这一样:在为Tic-Tac-Toe做“ pseudo-TDD”时,它们首先创建一个Board带有的3x3数组int(或类似的东西)。而如果您强迫他们执行TDDAIYMI,则他们通常最终会创建一个微型DSL来捕获领域知识。当然,那只是轶事。进行统计和科学合理的研究会很好,但是像这样的研究经常会发现,它们要么太小要么太昂贵。
约尔格W¯¯米塔格

@JörgWMittag如果我误解了您,请纠正我,但是您是说罗恩·杰弗里斯Ron Jeffries)在做“伪TDD”吗?这不是“没有真正的苏格兰人”谬论的一种形式吗?(我同意您需要进行更多的科学研究;我链接到的博客只是一个有趣的轶事,讲述了TDD使用特定实例的惊人失败。不幸的是,对于其他人来说,TDD宣传人员似乎太吵了我们对这种水met及其所谓的好处进行真实的分析)。
Andres F.

5

您将开发方法描述为“自上而下”的过程-从更高的抽象级别开始,然后越来越多地进入细节。TDD,至少以其流行的形式,是一种“自下而上”的技术。对于一个主要是“自上而下”工作的人来说,“自下而上”工作确实是非常不寻常的。

那么,如何在开发过程中引入更多的“ TDD”呢?首先,我假设您的实际开发过程并不总是如您上面所述那样“自上而下”。步骤2之后,您可能已经确定了一些与其他组件无关的组件。有时,您决定首先实现那些组件。这些组件的公共API的详细信息可能并不仅仅满足您的要求,这些详细信息还遵循您的设计决策。这是您可以从TDD开始的关键所在:想象一下如何使用组件,以及如何实际使用API​​。当您开始以测试形式对此类API用法进行编码时,您只是从TDD开始。

其次,即使要编写更多的“自上而下”的代码,也可以执行TDD,首先要从依赖于其他不存在的组件的组件开始。您需要学习的是如何首先“模拟”其他依赖项。这样一来,您可以在进入较低级别的组件之前创建和测试高级组件。在Ralf Westphal的此博客文章中,可以找到有关以自顶向下的方式进行TDD的非常详细的示例。


3

我做对了吗?如果不是,我到底要改变什么

你还好吧

有什么方法可以确定您编写的测试是否足够?

是的,请使用测试/代码覆盖率工具。马丁·福勒(Martin Fowler)在测试覆盖率方面提供了一些很好的建议

是针对非常简单的功能(相当于1 + 1 = 2)编写测试的好习惯吗?

通常,在某些输入下您希望产生某些结果的任何函数,方法,组件等都是单元测试的不错选择。但是,与(工程)生命中的大多数事情一样,您需要考虑一下权衡:通过编写单元测试来抵消这种工作量,从长远来看会导致更稳定的代码库吗?通常,选择首先为关键/关键功能编写测试代码。以后,如果您发现一些与未经测试的代码相关的错误,请添加更多测试。

更改功能并相应地测试需求是否有所变化是否很好?

进行自动化测试的好处是,您将立即看到更改是否破坏了先前的声明。如果由于需求变更而期望这样做,可以更改测试代码(实际上,在纯TDD中,您将首先根据需求更改测试,然后采用该代码,直到满足新的需求)。


代码覆盖率可能不是很可靠的措施。强制执行覆盖率百分比通常会导致很多不必要的测试(例如针对所有参数的空检查等测试,这些测试是为了使测试几乎没有任何价值),并且浪费了开发时间,同时难以测试代码路径可能根本没有经过测试。
保罗

3

首先编写测试是编写软件的完全不同的方法。测试不仅是正确的代码功能验证(全部通过)的工具,而且是定义设计的力量。尽管测试覆盖率可能是一个有用的指标,但它本身并不是目标-TDD的目标不是获得良好的代码覆盖率,而是在编写代码之前考虑一下代码的可测试性。

如果您首先遇到编写测试的麻烦,我强烈建议与TDD经验丰富的人进行一对编程会议,以便您获得有关整个方法“思考方式”的经验。

另一件好事是观看在线视频,其中从一开始就使用TDD开发软件。我曾经用来向自己介绍TDD的好人是James Shore的Let's Play TDD。看一下,它将说明紧急设计的工作原理,编写测试时应问自己什么问题,以及如何创建,重构和迭代新的类和方法。

有什么方法可以确定您编写的测试是否足够?

我相信这是一个错误的问题。在进行TDD时,您选择进行TDD和紧急设计作为编写软件的方式。如果您需要添加的任何新功能始终以测试开始,那么它将始终存在。

是针对非常简单的功能(相当于1 + 1 = 2)编写测试的好习惯吗?

显然这取决于您的判断。如果该方法不是公共API的一部分,则我不希望对参数的空检查编写测试,但否则,为什么不确认方法Add(a,b)确实返回a + b呢?

更改功能并相应地测试需求是否有所变化是否很好?

同样,当您更改代码或向代码中添加新功能时,您将从测试开始,无论是添加新测试还是在需求变化时更改现有测试。

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.