【问题标题】:Why is design-by-contract not so popular compared to test-driven development?为什么与测试驱动开发相比,按合同设计不那么受欢迎?
【发布时间】:2010-10-03 15:18:19
【问题描述】:

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

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

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

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

TDD

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

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

DBC

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

我个人喜欢 DBC 方法。


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

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

【问题讨论】:

    标签: unit-testing tdd design-by-contract


    【解决方案1】:

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

    【讨论】:

      【解决方案2】:

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

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

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

      【讨论】:

        【解决方案3】:

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

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

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

        【讨论】:

        • Eiffel 有很多有趣的想法,只是还没有为主流使用做好准备。我的一群朋友正在研究非常酷的 Java 的 DBC 形式。我一直致力于通过形式化“指令”来改进文档
        • 对合约细节的静态分析也不适用于 Eiffel,它只有主格类型检查,一个非常好的实现,但不是特定于合约的。
        • 文档会过时。开发人员不会阅读它(可能是因为它可能已经过时了)。您必须单独运行测试,即使那样它们也非常可忽略。 DbC 的前置/后置条件和不变量执行内联,每次调用,不可绕过。
        • 如果你使用Python,你可以使用github.com/Parquery/icontract定义合约并使用github.com/mristin/icontract-hypothesis自动生成测试或使用github.com/pschanely/CrossHair进行静态分析。所以我们正在使用 Python。
        【解决方案4】:

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

        Bertrand Meyer 的书面向对象的软件构建,第 2 版并没有说你永远不会犯错误。事实上,如果你看一下“当合约被破坏”一章,它会讨论当一个函数无法完成它的合约状态时会发生什么。

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

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

        【讨论】:

          【解决方案5】:

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

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

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

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

          【讨论】:

          • 您可以简单但缓慢地实现散列函数,并确保您的最佳实现返回相同的答案。然后,您可以在制作中关闭此特定合同。
          【解决方案6】:

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

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

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

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

          【讨论】:

          • 我发现 DbC 在创建原型时很有用,因为我对数据模型和业务规则没有清晰的概念,因此我在搜索可行的应用程序设计时对代码进行了重大更改。在这种情况下,TDD 会减慢对好的设计的搜索速度。 DbC 不会减慢更改设计的速度。我开发的应用程序对数据库、文件和文件中的元数据执行复杂的操作,由于无法轻松隔离通常读取实时数据以进行单元测试的代码,因此我在使用 TDD 时会遇到问题。
          • 不确定 DbC 如何允许 运行时快速失败 比 TDD 更多,因为理论上合约应该通过涵盖所有可能的情况来提供更全面的编译时检查,而单元测试仅在编译时涵盖一些硬编码的情况。
          【解决方案7】:

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

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

          【讨论】:

            【解决方案8】:

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

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

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

            所以两者是互补的。

            格雷格

            【讨论】:

            • “如果你只有一把锤子作为工具,那么一切都开始像钉子”。我一直在尝试阅读 TDD,但我读的越多,听起来越像歇斯底里;读一些更冷静的东西是令人耳目一新的。 TDD 是一个工具,仅此而已。
            【解决方案9】:

            首先,我是 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-ish 代码的一部分执行。因为这是真的,所以我们在代码中放置的合约断言(测试)不必出现在我们的 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 是执行合同的一种催化剂。

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

            【讨论】:

            • 经过多年的思考和反思。我现在知道它不是 TDD 与 DBC,而是 TDD + DBC
            猜你喜欢
            • 2019-06-11
            • 2014-05-24
            • 2020-10-25
            • 2016-01-20
            • 1970-01-01
            • 2012-11-16
            • 1970-01-01
            • 1970-01-01
            • 2021-04-04
            相关资源
            最近更新 更多