如果我需要设计来开始测试,我不明白TDD如何帮助我获得良好的设计


50

我试图围绕TDD,特别是开发部分,全神贯注。我看过一些书,但是我发现的书主要涉及测试部分-NUnit的历史,为什么测试很好,红色/绿色/重构以及如何创建字符串计算器。

好东西,但这只是“单元测试”,而不是TDD。具体来说,如果我需要一个设计来开始对其进行测试,我不理解TDD如何帮助我获得一个好的设计。

为了说明这一点,请想象一下这三个要求:

  • 目录需要有产品列表
  • 目录应记住用户查看过的产品
  • 用户应该可以搜索产品

在这一点上,许多书从书本上摘下来,只是跳入“测试ProductService”,但他们没有解释如何得出结论,即首先有ProductService。这就是我要理解的TDD中的“开发”部分。

需要有一个现有的设计,但是实体服务之外的东西(也就是说:有一个Product,所以应该有一个ProductService)找不到地方(例如,第二个要求要求我有一个用户,但是我应该在哪里提醒该功能?搜索是ProductService的功能还是单独的SearchService?我怎么知道应该选择哪个?)

根据SOLID,我将需要一个UserService,但是如果我设计一个没有TDD的系统,则可能会得到一堆完整的Single-Method Services。TDD并不是一开始就让我发现自己的设计吗?

我是.net开发人员,但Java资源也可以使用。我觉得似乎没有真正的示例应用程序或书籍可以处理真正的业务应用程序。有人可以提供一个清晰的示例来说明使用TDD进行设计的过程吗?


2
TDD只是整个开发方法论的一部分。当然,您将需要采用某种设计(前期设计或更好的进化设计)来使整个过程融合在一起。
欣快感,

3
@gnat:这是对为何TDD书籍没有使设计过程更清晰的一个疑问。
罗伯特·哈维

4
@gnat:是您的编辑,不是我的。:)看到我对问题标题和正文的更改。
罗伯特·哈维

9
如果您阅读过Robert C. Martin的作品,或者看过他的视频之一,您会发现他经常想到一个设计,但他并不喜欢它。他认为,他对正确设计的先入为主的概念将在他的测试中浮现出来,但他并没有强迫这样做。最后,有时这种设计可以实现,有时却不能。我的意思是,您自己的过往经验将指导您,但测试应能带您前进。测试应该能够开发或取消设计。
Anthony Pegram

3
因此,这与测试无关,而与设计有关。只是它并没有真正帮助您进行设计,而是帮助您验证设计。但这不是!@#$ ing测试吗?
Erik Reppen

Answers:


17

TDD的想法是从测试开始并从此开始工作。因此,以您的“目录需要具有产品列表”为例,可以将其视为“检查目录中的产品”测试,因此这是第一个测试。现在,拥有目录的是什么?什么持有产品?这些是下一部分,其思想是将一些零碎的东西放在一起,就像ProductService之类的东西,它将通过使第一个测试通过而产生。

TDD的想法是从测试开始,然后编写使该测试通过的代码为第一要点。单元测试是“是”的一部分,但是您不会查看通过开始测试然后编写代码而形成的总体情况,因此在这一点上不会出现盲点,因为还没有任何代码。


测试驱动开发教程,其中幻灯片20-22是关键部分。这样做的目的是要知道功能的结果,为它编写一个测试,然后构建一个解决方案。设计部分将根据所需的内容而有所不同,它可能会或可能不那么容易做到。关键一点是从一开始就使用TDD,而不要尝试将其引入项目中。如果您首先进行测试,这会有所帮助,并且在某种意义上可能是值得注意的。如果稍后尝试添加测试,则可能会推迟或延迟测试。后面的幻灯片也可能有用。


TDD的主要好处是,从测试开始,您就不会一开始就被设计所束缚。因此,其想法是构建测试并创建将通过这些测试的代码作为开发方法。一个大的设计在前面可能会出现问题,因为这给了锁的东西到位,这使得正在构建的系统将在年底不再那么敏捷的想法。


罗伯特·哈维(Robert Harvey)在评论中补充了这一点,值得在答案中说明:

不幸的是,我认为这是对TDD的普遍误解: 您不能仅通过编写单元测试并使它们通过来发展软件体系结构。编写单元测试确实会影响设计,但不会创建设计。您必须这样做。


31
@MichaelStum:不幸的是,我认为这是对TDD的普遍误解:您不能仅通过编写单元测试并使其通过来发展软件体系结构。 编写单元测试确实会影响设计,但不会创建设计。 必须这样做。
罗伯特·哈维

4
@ RobertHarvey,JimmyHoffa:如果我能将您的评论投票100次,我会的!
布朗

9
@Robert Harvey:我很高兴您写了这个常见的误解:我经常听到有人坐下来编写各种单元测试的消息,而设计只会自发地“出现”。而且,如果您的设计不好,那是因为您没有编写足够的单元测试。我同意您的看法,测试是一种用于指定和验证设计要求的工具,但是您必须“自己做”设计。我完全同意。
乔治

4
@ Giorgo,RobertHarvey:我也向RobertHarvey +1000。不幸的是,这种误解很普遍,以至于一些“专家” TDD /敏捷从业者认为这是真的。举例来说,他们假装您可以在没有领域知识或任何种类的分析的情况下,从TDD中“发展”一个数独求解器。我想知道罗恩·杰弗里斯(Ron Jeffries)是否曾经发表过有关TDD局限性的后续报告,或者解释了为什么他突然停止实验而没有任何结论或经验教训。
Andres F.

3
@Andres F:我知道关于数独的故事,我认为这很有趣。我认为一些开发人员会误以为工具(例如TDD或SCRUM)可以代替领域知识和他们自己的努力,并期望通过机械地应用特定方法,好的软件将神奇地“出现”。他们通常是不喜欢在分析和设计上花费太多时间,而是喜欢直接编写代码的人。对于他们来说,遵循特定的方法是没有进行适当设计的理由。但这是恕我直言,滥用TDD。
Giorgio 2013年

8

就其价值而言,TDD可以帮助我比不进行TDD 更快地达到最佳设计。有或没有它,我可能都会得出最佳设计。但是,我本来会花时间思考并花一些时间在代码上的时间却花在编写测试上。而且时间更少。为了我。并不适合所有人。而且,即使花费了相同的时间,它也会给我带来一系列测试,从而使重构更加安全,从而使代码更加完善。

它是如何做到的?

首先,它鼓励我考虑将每个类视为对某些客户端代码的服务。更好的代码来自于考虑调用代码如何使用API​​而不是担心代码本身的外观。

其次,这使我无法考虑一种方法,而将太多的圈算复杂性写入一种方法。通过方法的每条额外路径都会使我需要进行的测试数量增加一倍。纯粹的惰性决定了在我添加过多的逻辑之后,我必须编写16个测试来添加一个条件,是时候将其中的一些条件拉到另一个方法/类中并分别对其进行测试了。

真的就是这么简单。这不是魔术设计工具。


6

我试图围绕TDD展开思考...为了说明这一点,请想象一下这3个要求:

  • 目录需要有产品列表
  • 目录应记住用户查看过的产品

这些要求应以人为名义重新陈述。谁想知道用户以前浏览过哪些产品?用户?一名销售员?

  • 用户应该可以搜索产品

怎么样?按名字?按品牌?测试驱动开发的第一步是定义一个测试,例如:

browse to http://ourcompany.com
enter "cookie" in the product search box
page should show "chocolate-chip cookies" and "oatmeal cookies"

>

在这一点上,许多书从书本上摘下来,只是跳入“测试ProductService”,但他们没有解释如何得出结论,即首先有ProductService。

如果仅是这些要求,那么我当然不会急于创建ProductService。我可能会创建一个带有静态产品列表的非常简单的网页。在您达到添加和删除产品的要求之前,这将非常有效。到那时,我可能会决定使用关系数据库和ORM并创建映射到单个表的Product类是最简单的。仍然没有ProductService。当需要时,将创建诸如ProductService之类的类。可能有多个Web请求需要执行相同的查询或更新。然后将创建ProductService类,以防止代码重复。

总而言之,TDD驱动着要编写的代码。在选择实现时进行设计,然后将代码重构为类,以消除重复和控件依赖性。添加代码时,您将需要创建新的类以使代码保持SOLID。但是,您无需提前决定是否需要Product类和ProductService类。您可能会发现,仅使用Product类,生活就很好了。


OK,没ProductService那么。但是TDD如何告诉您您需要数据库和ORM?
罗伯特·哈维

4
@罗伯特:没有。根据我对满足要求的最有效方法的判断,这是一个设计决策。但是决定可能会改变。
凯文·克莱恩

1
好的设计永远不会因为某些任意过程而产生副作用。拥有可用于处理和构架事物的系统或模型很棒,但是IMO通过测试先行的TDD产生了利益冲突,因为它可以保证人们不会因不良后果而被意外地咬伤本来不应该发生的代码。设计需要反思,意识和前瞻性。您不会从修剪掉树上的自动发现症状中学到这些。通过首先弄清楚如何避免邪恶的突变分支来学习它们。
Erik Reppen 2013年

我认为测试“添加了产品;重新启动计算机并重新启动系统;添加的产品仍然应该可见。” 显示了对某种数据库的需求从何而来(但仍然可以是平面文件或XML)。
yatima2975 2013年

3

其他人可能会不同意,但是对我而言,许​​多较新的方法论均基于以下假设:开发人员将出于习惯或个人自豪感而执行大多数较旧方法论所阐明的内容,即开发人员通常所做的事情相当明显对他们而言,工作被封装在一种简洁的语言中,或者在某种程度上显得有些凌乱。

过去我曾遇到过的一些示例:

  • 拿一大堆规范工作的承包商,告诉他们他们的团队是敏捷和测试优先的。他们通常除了按规范工作以外没有其他习惯,只要工作时间足以完成项目,他们就不会担心工作质量。

  • 首先尝试做一些新的测试,然后花很多时间进行测试,因为发现各种方法和接口都是废话。

  • 编写一些低级的代码,要么因为缺乏覆盖而被打败,要么编写了大量的测试,这些测试的价值不高,因为您无法模拟所绑定的基本行为。

  • 在任何情况下,如果您没有足够的基础机制来提前添加可测试的功能,而没有先编写大量基础不可测试的位,例如磁盘子系统或tcpip级别的通信接口。

如果您正在执行TDD,并且对您有用,对您也有好处,但是有很多事情(整个工作或项目的各个阶段)根本无法增加价值。

您的示例听起来好像还没有设计,因此要么需要进行架构对话,要么要进行原型设计。我认为您首先需要解决其中的一些问题。


1

我坚信TDD是系统详细设计(即API和对象模型)的一种非常有价值的方法。但是,要达到在项目中开始使用TDD的目的,您需要已经以某种方式对设计进行了总体建模,并且需要已经以某种方式对架构进行了建模。@ user414076解释了罗伯特·马丁(Robert Martin)的设计思想,但并未与之结婚。究竟。结论-TDD不是正在进行的唯一设计活动,而是设计细节的充实方式。在TDD之前必须进行其他设计活动,并使其适合于解决总体设计如何创建和演化的总体方法(例如敏捷)。

仅供参考-我推荐有关该主题的两本书,提供了切实可行的示例:

在测试的指导下,不断发展的面向对象软件 -解释并给出完整的项目示例。这是一本关于设计而不是测试的书。测试被用作在设计活动中指定预期行为的一种手段。

测试驱动的开发实用指南 -逐步开发完整的,虽然很小的应用程序,但循序渐进。


0

TTD会通过测试失败而不是成功来推动设计发现,因此您可以测试未知项,并通过反复测试来进行测试,因为最终会暴露出未知数,从而最终形成完整的单元测试工具,这对于正在进行的维护是一件非常好的事情,而要尝试进行一件非常困难的事情编写/发布代码后进行改进。

例如,可能的要求是输入可以采用几种不同的格式,但尚不完全知道。使用TDD,您首先要编写一个测试,以验证是否提供了给定的任何输入格式的输出。显然,该测试将失败,因此您需要编写代码来处理已知格式并重新测试。由于未知的格式是通过需求收集暴露出来的,因此在编写代码之前要编写新的测试,这些测试也应该失败。然后编写新代码以支持新格式,并重新运行所有测试,以减少回归的机会。

将单元故障视为“未完成”代码而不是“损坏”代码也很有帮助。TDD允许未完成的单元(意外故障),但减少了损坏的单元(意外的故障)的发生。


1
我确实同意这是一个有效的工作流程,但是并不能真正解释这种工作流程如何形成高级架构。
罗伯特·哈维

1
正确,像MVC模式这样的高级体系结构不会仅从TDD中出现。但是,从TDD中可以得出的是设计易于测试的代码,这本身就是设计考虑因素。
Daniel Pereira

0

问题中指出:

...许多书籍从书本上脱颖而出,只是深入研究“测试ProductService”,但是他们没有解释如何得出结论,即首先有ProductService。

他们通过考虑如何测试该产品来得出该结论。“这是什么产品?” “好吧,我们可以创建服务”。“好吧,让我们为这种服务编写一个测试”


0

一个功能可以有很多设计,而TDD不会完全告诉您哪个是最好的。即使测试可以帮助您构建更多的模块化代码,它也可以引导您构建适合测试要求的模块,而不是适合实际生产的模块。因此,您必须了解您要去往何处以及事物应如何适合整个情况。否则,存在功能和非功能需求,请不要忘记最后一个。

关于设计,我参考Robert C. Martin的书籍(敏捷开发),也参考Martin Fowler的企业应用程序体系结构和域驱动程序设计模式。后者在从需求中提取实体和关系时非常有系统。

然后,当您对如何管理这些实体有了很好的选择时,可以采用TDD方法。


0

TDD并不是一开始就让我发现自己的设计吗?

没有。

您如何测试尚未设计的东西?

为了说明这一点,请想象一下这三个要求:

  • 目录需要有产品列表
  • 目录应记住用户查看过的产品
  • 用户应该可以搜索产品

这些不是要求,这些是数据的定义。我不知道您的软件是做什么的,但分析师不太可能这样说。

您需要知道系统的不变式。

需求将类似于:

  • 如果库存中有足够数量的产品,则客户可以订购一定数量的产品。

因此,如果这是唯一的要求,则您可能需要一个类似以下的类:

public class Product {

  private int quantity;

  public Product(int initialQuantity) {
    this.quantity = initialQuantity;
  }

  public void order(int quantity) {
    // To be implemented.
  }

}

然后使用TDD,您将在实现order()方法之前编写一个测试用例。

public void ProductTest() {

    public void testCorrectOrder() {

        Product p = new Product(10);
        p.order(3);
        p.order(4);

    }

    @Expect(ProductOutOfStockException)
    public void testIncorrectOrder() {

        Product p = new Product(10);
        p.order(7);
        p.order(4);

    }

}

因此第二次测试将失败,然后您可以按照自己的方式实现order()方法。


0

您说得很对,TDD可以很好地实现给定的设计。它不会帮助您的设计过程。


但是,它为您提供了安全网,可以在不破坏工作代码的情况下改进设计。这是大多数人跳过的重构。
阿德里安·施耐德

-3

TDD很有帮助,但是在软件开发中有重要的组成部分。开发人员应收听正在编写的代码。重构是TDD周期中的第三部分。这是开发人员在进行下一个红色测试之前应集中精力和思考的主要步骤。有重复吗?是否使用SOLID原则?高凝聚力和低耦合又如何呢?那名字呢?仔细查看测试中出现的代码,看看是否需要更改,重新设计。对该代码有疑问,代码将告诉您如何设计。 我通常编写多个测试的集合,检查该列表并创建第一个简单的设计,它不必是“最终的”,通常不是,因为在添加新测试时已更改。设计就是在那里。

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.