43

你可能会认为这个问题就像之前在 StackOverflow 上提出的这个问题。但我试图以不同的方式看待事物。

在 TDD 中,我们编写包含不同条件、标准、验证码的测试。如果一个班级通过了所有这些测试,我们就可以开始了。这是一种确保班级实际上做它应该做的事情而不是其他任何事情的方法。

如果您逐字遵循Bertrand Meyers的《面向对象的软件构建》一书,则该类本身具有内部和外部契约,因此它只做它应该做的事情,而不做其他事情。不需要外部测试,因为确保遵循合同的代码是类的一部分。

让事情变得清晰的快速示例

TDD

  1. 创建测试以确保在所有情况下,值的范围都是 (0-100)

  2. 创建一个包含通过测试的方法的类。

DBC

  1. 创建一个类,为该成员创建一个var范围为(0-100)的合同,设置合同违约合同,定义一个方法。

我个人喜欢 DBC 方法。


纯DBC不那么受欢迎是有原因的吗?是语言、工具还是敏捷,还是只有我喜欢让代码自己负责?

如果您认为我的想法不对,我会非常愿意学习。

4

9 回答 9

35

DBC 的主要问题是,在绝大多数情况下,要么无法正式指定合约(至少不方便),要么无法使用当前的静态分析工具进行检查。在我们超越主流语言(不是埃菲尔)的这一点之前,DBC 不会提供人们需要的那种保证。

在 TDD 中,测试是由人根据方法的当前自然文本规范编写的,这些规范(希望)是有据可查的。因此,人类通过编写测试来解释正确性,并根据该解释获得一些保证。

如果您阅读 Sun 编写 JavaDocs 的指南,它说该文档应该基本上列出一份足以编写测试计划的合同。因此,按合同设计不一定与 TDD 相互排斥。

于 2009-01-26T21:18:51.380 回答
24

TDD 和 DbC 是两种不同的策略。DbC 允许在运行时快速失败,而 TDD 在“编译时”采取行动(确切地说,它在编译后立即添加一个新步骤以运行单元测试)。

这是 TDD 相对于 DbC 的一大优势:它允许获得更早的反馈。当您以 TDD 方式编写代码时,您会同时获得代码及其单元测试,您可以根据您在测试中编码的您认为它应该的内容来验证它是否“有效”。使用 DbC,您可以获得带有嵌入式测试的代码,但您仍然需要练习它。IMO,这当然是 Dbc 不那么受欢迎的原因之一。

其他优点:TDD 创建了一个自动测试套件,允许检测(防止读取)回归并使重构安全,因此您可以逐步扩展您的设计。DbC 不提供这种可能性。

现在,使用 DbC 快速故障非常有用,尤其是当您的代码与其他组件接口或必须依赖外部源时,在这种情况下测试合约可以节省您的时间。

于 2009-01-27T07:29:23.787 回答
21

首先,我是 Eiffel 的软件工程师,所以我可以从经验中谈谈这件事


TDD vs DbC的前提是不正确的

这两种技术并不矛盾,而是相辅相成。补语与断言和目的的放置有关。

TDD 的目的既有组件又有范围。TDD 的基本组件是布尔断言和对象特征(例如方法)执行。步骤很简单:

  1. 创建一个对象。
  2. 在功能中执行一些代码。
  3. 对对象上的数据状态进行断言。

断言失败,测试失败。通过所有断言是目标。

与 TDD 一样,Design-by-Contract 的合同具有目的、范围和组件。虽然 TDD 仅限于单元测试时间,但合同可以贯穿整个 SDLC(软件开发生命周期)!在 TDD 范围内,对象方法(特性)的执行将执行合同。在 Eiffel Studio 自动测试 (TDD) 设置中,创建一个对象,进行调用(就像其他语言中的 TDD 一样),但这里是相似的地方。

在具有自动测试功能的 Eiffel Studio 和具有合同的 Eiffel 代码中,目的有所改变。我们要测试客户-供应商关系。我们的 TDD 代码在其对象上伪装成我们的 Supplier 方法的客户。我们创建对象并基于此目的调用方法,而不仅仅是简单的“TDD-ish 方法测试”。因为对我们的方法(功能)的调用有契约,这些契约将作为我们在自动测试中的 TDD 式代码的一部分执行。因为这是真的,所以我们在代码中放置的合约断言(测试)不必出现在我们的 TDD 测试代码中。我们(作为程序员)的工作是简单地确保:A)代码+合约沿所有 N 路径执行,以及 B)代码+合约使用所有合理的数据类型和范围执行。

关于 TDD-DbC 互补关系可能还有更多要写的,但我不会在这件事上对你粗鲁。可以说 TDD 和 DbC 与其他没有矛盾——绝对不是!

DbC 合约的力量超出了 TDD 所能达到的范围

现在,我们可以将注意力转向按合同设计的合同的力量,超越 TDD 的能力!

合约存在于代码中。它们不是外部的,而是内部的。关于合约最强大的一点(超出他们的客户-供应商合约关系基础)是编译器旨在了解它们!它们不是埃菲尔铁塔的附加物!因此,它们参与了继承的各个方面(传统的垂直 is-a 继承和横向或水平泛型)。此外,它们到达了 TDD 无法到达的地方——方法(特征)内部。

虽然 TDD 可以轻松地模拟前置条件和后置条件,但 TDD 无法进入代码内部并执行循环不变合约,也无法在代码块执行时定期抽查“检查”合约。这是一个强大的逻辑和定性范式,也是按合同设计如何运作的现实。

此外,TDD 不能做类不变量,只能以最微弱的方式。我已经尽我最大的努力让我的自动测试代码(实际上只是应用 TDD 的 Eiffel Studios 版本)来进行类不变模仿。这不可能。要了解为什么您必须了解 Eiffel 类不变量如何工作的来龙去脉。因此,目前,您只需要相信(或不相信)TDD 无法完成这项任务,而 DbC 可以如此轻松、良好、优雅地处理!

DbC 的影响并不止于上述概念

我们在上面提到,TDD 存在于单元测试时。契约,因为它们在编译器的监督和控制下应用在代码中,所以适用于代码可以执行的任何地方:

  1. 工作台:作为程序员,您正在使用代码来查看它的工作情况(例如,在 TDD 时间之前或与 TDD 时间结合使用)。

  2. 单元测试:您的持续集成测试、单元测试、TDD 等。

  3. Alpha 测试:您的初始测试用户在运行可执行文件时会被合约绊倒

  4. Beta 测试:更广泛的用户受众也将被合同绊倒。

  5. 生产:最终的可执行文件(或生产系统)可以通过合同进行持续测试(TDD 不能)。

在上述每种情况下,您都会发现自己可以控制运行哪些合约以及从哪些来源!您可以选择性地、细粒度地打开和关闭各种形式的合约,并以极高的精度控制编译器在何时何地应用它们!

如果这一切还不够,合约(通过设计)可以做一些 TDD 断言永远无法做到的事情:告诉您调用堆栈中的哪个位置以及哪个客户端-供应商关系被破坏,以及为什么(这也立即表明如何要解决这个问题)。为什么这是真的?

TDD 断言旨在告诉您事后代码运行(执行)的结果。TDD 断言只能看到被检查方法的当前状态。TDD 断言不能从它们在代码库外部的位置做的是准确地告诉您哪个调用失败以及为什么!您会看到——您对某个方法的初始 TDD 调用将触发该方法。很多时候,那个方法会调用另一个,另一个,另一个——有时,随着调用堆栈的来回摆动,会出现中断:数据写入错误,根本不写入,或者写入它不应该的时候。

TDD就像谋杀已经发生后出现在犯罪现场的警察。他们只剩下法医线索,他们希望这些线索能将他们引向嫌疑人并定罪。但是,如果我们可以在犯罪发生时在那里呢?这就是放置 TDD 断言和合同断言之间的区别。合同是用来捕捉正在进行的犯罪的,它们直接指向犯罪者,因为它正在犯罪!

回顾

让我们回顾一下。

  • TDD 与 DbC 并不矛盾。

  • 它是一组技术的补充和协作,但具有不同的功能和目的,以及使用它们的工具。

  • 合同更进一步,并在代码中断时揭示更多有关您的代码的信息。

  • TDD 是执行合同的一种催化剂。

在一天结束时:我想要两个!在阅读完所有这些之后(如果你幸存下来),我希望你也能这样做。

于 2015-02-23T18:27:54.133 回答
12

按合同设计和测试驱动开发并不相互排斥

Bertrand Meyer 的书Object Oriented Software Construction, 2nd Edition并没有说你永远不会犯错误。事实上,如果你看一下“当合约被破坏”一章,它会讨论当一个函数无法完成它的合约状态时会发生什么。

您使用 DbC 技术这一简单事实并不能使您的代码正确。合同设计以合同的形式为您的代码及其用户建立明确定义的规则。这很有帮助,但无论如何你总是会把事情搞砸,只是你可能会更早注意到。

测试驱动开发将从外部世界以黑盒方式检查您的类的公共接口是否正确运行。

于 2009-01-26T21:20:04.560 回答
8

我认为最好将这两种方法结合使用,而不是仅使用其中一种。

在我看来,在类及其方法本身中完全执行契约是不切实际的。

例如,如果一个函数说它将通过某种方法散列一个字符串并将散列后的字符串作为输出返回,那么该函数如何强制该字符串被正确散列?再次哈希它,看看它们是否匹配?看起来很傻。反转散列,看看你是否得到原件?不可能。相反,您需要一组测试用例来确保函数正确运行。

另一方面,如果您的特定实现要求您的输入数据具有一定的大小,那么建立一个合同并在您的代码中执行它似乎是最好的方法。

于 2009-01-26T21:22:35.743 回答
6

在我看来,TDD 更“归纳”。您从示例(测试用例)开始,您的代码体现了这些示例的通用解决方案。

DBC 似乎更“演绎”,在收集需求后确定对象行为和合同。然后,您对这些合约的具体实现进行编码。

编写合约有些困难,比作为具体行为示例的测试更困难,这可能是 TDD 比 DBC 更受欢迎的部分原因。

于 2009-01-26T21:14:34.213 回答
5

我看不出两者不能共存的理由。看一个方法,一眼就知道合约到底是什么,真是太好了。很高兴知道我可以运行我的单元测试并且知道我的最后一次更改没有破坏任何东西。这两种技术并不相互排斥。为什么按合同设计不受欢迎是一个谜。

于 2009-01-26T21:00:46.347 回答
5

我过去都使用过,发现DBC 风格不太“侵入性”。DBC 的驱动程序可能是常规应用程序运行。对于单元测试,您必须注意设置,因为您期望(验证)一些响应。对于 DBC,您不必这样做。规则以独立于数据的方式编写,因此无需设置和模拟。

更多关于我使用 DBC/Python 的经验:http: //blog.aplikacja.info/2012/04/classic-testing-vs-design-by-contract/

于 2012-09-18T21:00:40.847 回答
5

我将按合同设计视为所有案例中成功/失败的规范,而测试驱动开发针对一个特定案例。如果 TDD 案例成功,则假定一个函数正在完成它的工作,但它没有考虑其他可能导致它失败的案例。

另一方面,按合同设计并不一定要保证所需的答案,只需保证答案是“正确的”。例如,如果一个函数返回应该返回一个非空字符串,那么您在 ENSURE 中唯一可以假设的是它不会为空。

但也许它不会返回预期的字符串。合约无法确定这一点,只有测试可以表明它正在按照规范执行。

所以两者是互补的。

格雷格

于 2013-07-17T19:26:48.953 回答