设计契约和测试驱动开发

28

我正在改进我们团队的开发流程,考虑最佳实践在测试驱动开发中如何实现设计契约。两种技术似乎有很大的重叠,我想知道以下相关问题的一些见解:

  • 如果没有使用某种代码生成器根据契约生成单元测试,TDD和DbC是否违反了DRY原则?否则,您必须在两个位置(测试和契约本身)维护契约,或者我错过了什么?
  • TDD在多大程度上使DbC变得多余?如果我编写得足够好的测试,它们是否等同于编写契约?只有将契约在运行时以及通过测试进行强制执行,才会获得额外的好处吗?
  • 仅使用TDD比同时使用TDD和DbC更容易/更灵活吗?

这些问题的主要点是这个更一般的问题:如果我们已经适当地执行TDD,那么如果我们也使用DbC,我们会获得显着的效益吗?

尽管我认为问题基本上与语言无关,但还有一些细节:

  • 我们的团队非常小,少于10名程序员。
  • 我们主要使用Perl。
8个回答

39
请注意不同之处。
以合同为驱动的设计。 契约驱动设计。
通过测试驱动开发。 测试驱动开发。
它们相关,因为其中一个先于另一个。 它们描述了不同抽象级别的软件。
在实施时是否丢弃设计? 您是否认为设计文档违反DRY原则? 是否将合同和代码分开维护?
软件是合同的一种实现。 测试是另一种实现。 用户手册是第三种实现。 操作指南是第四种。 数据库备份/还原程序是合同实现的一部分。
我看不到契约设计中的任何额外负担。
如果您已经在进行设计,则只需更改格式即可从过多的文字转化为概述合同关系的恰当词语。
如果您没有进行设计,则编写合同将消除问题,减少成本和复杂性。
我看不到任何灵活性的损失。
首先是合同,
然后
a.编写测试和
b.编写代码。
请看到这两个开发活动本质上是交织在一起的,而且都来自合同。

+1. 很棒的答案。初步合约的开发也是一项很好的活动,可以将其作为艺术品交给初级和中级开发者处理,前提是您没有架构师或领导来进行一些原型设计。 - Joseph Ferris
非常好的答案,这正是我在寻找的。感谢您直接简洁地解释了两者之间的区别(和关系)。 - Adam Bellaire
2
@regu.pattabi:我不能再清楚了:“1. 从一份合约开始, 然后 a. 编写测试和 b. 编写代码。” - S.Lott
我不明白你是如何在编写代码之前就编写合同的。合同是代码的一部分。我的做法是“1. 如果需要,先编写测试;2. 然后编写方法 a. 先编写合同,然后 b. 编写内容。”接着,a. 编写测试b. 编写代码。 - Bastien Vandamme
2
@Dran Dane:放松点。先写一些代码,再写一些测试,然后再写其余的工作代码是可以的。不要过于法律化地认为“合同”和“代码”是如此不同,以至于整个过程永远无法实现。如果有帮助,您可以在测试之前编写接口。应该避免先编写整个实现。有时候,对于琐碎的代码,你会先写所有的代码。没关系。只要避免在测试之前编写大型实现即可。 - S.Lott
显示剩余3条评论

27

我认为DbC和TDD之间存在重叠,但我认为它们并没有重复的工作:引入DbC可能会导致测试用例的减少。

让我解释一下。

在TDD中,测试实际上不是测试,它们是行为规范。然而,它们也是设计工具:通过先编写测试,您使用待测试对象的外部API - 实际上您尚未编写的API - 以与用户相同的方式进行操作。这样,您可以按照对用户有意义的方式设计API,而不是按照最容易实现的方式。例如:queue.full?而不是queue.num_entries == queue.size

这第二部分不能由合约替代。

至少对于单元测试,第一部分可以部分地由合约替代。TDD测试用作行为规范,既针对其他开发人员(单元测试),也针对领域专家(验收测试)。合约也指定行为,对其他开发人员、领域专家、编译器和运行时库都是如此。

但约束具有固定的粒度:您具有方法前置条件和后置条件、对象不变式、模块合约等。也许还有循环变量和不变量。然而,单元测试测试的是行为单元。这些可能比方法更小,或由多个方法组成。这是您不能使用合约完成的。对于“大局”,仍需要进行集成测试、功能测试和验收测试。

还有TDD中DbC无法涵盖的另一个重要部分:中间的D。在TDD中,测试驱动您的开发过程:除非有一个失败的测试,否则您永远不会写一行实现代码;除非所有测试都通过,否则您永远不会写一行测试代码;您只编写最少量的实现代码以使测试通过;您只编写最少量的测试代码以产生失败的测试。

总之:使用测试来设计API的“流程”和“感觉”。使用合约来设计API的合约。使用测试为开发过程提供“节奏”。

类似如下:

  1. 为一个特性编写验收测试
  2. 为实现该特性某个部分的单元编写单元测试
  3. 使用你在步骤 2 中设计的方法签名,编写方法原型
  4. 添加后置条件
  5. 添加前置条件
  6. 实现方法体
  7. 如果验收测试通过,则回到步骤 1,否则返回步骤 2

如果您想了解 Design by Contract 的发明人 Bertrand Meyer 对于将 TDD 和 DbC 结合的看法,可以查看他的团队撰写的一篇不错的论文,名为 基于契约的设计 = 测试驱动开发 - 编写测试案例。基本前提是契约提供了所有可能情况的抽象表示,而测试案例只测试具体情况。因此,适当的测试框架可以从这些契约中自动生成。


6
我想补充一点:
API是程序员的契约,UI定义是与客户的契约,协议是客户端和服务器交互的契约。首先要明确这些,然后才能利用并行开发轨道,避免陷入细节中。是的,定期审核以确保满足要求,但不要在没有契约的情况下开始新的轨道。而“契约”是一个强有力的词:一旦部署,就不能更改。您应该从一开始就包括版本管理和内省,只有通过扩展集实现对合同的更改,版本号随之更改,然后可以在处理混合或旧安装时执行优雅的降级。
我通过一个大型项目吃了个大亏,它漫无目的地游荡,然后在严重的时间压力下,公司生存的短暂时间内正确地应用了它。我们定义了协议,为每个事务的每一侧定义和编写了一组协议仿真(基本上是罐头消息生成器和接收消息检查器,两个晚上的双脑编码),然后分别编写了应用程序的服务器和客户端端。我们在演出当晚重新组合,结果完美无缺。需求、设计、契约、测试、代码、集成。按照这个顺序重复,直到完成。
我有点担心按TLA设计。与模式一样,符合流行术语的配方是一个好指南,但我的经验是,没有一种适用于所有设计或项目管理程序的通用方法。如果您正在精确地按照书上的做法(tm),那么除非它是具有DOD程序要求的DOD合同,否则您可能会在某个地方遇到麻烦。阅读书籍,是的,但一定要理解它们,并考虑团队的人员方面。只有通过书本强制执行的规则不会得到统一执行-即使是在工具强制执行时也可能存在退出(例如svn注释留空或简洁)。只有在工具链不仅强制执行它们,而且使遵循比任何可能的捷径都更容易时,程序才倾向于遵循。相信我,当情况变得困难时,就会找到捷径,而您可能不知道在凌晨3点使用了哪些捷径,直到为时已晚。

2

1

微软在自动生成单元测试方面进行了一些工作,基于代码合同和参数化单元测试。例如,合同规定在将项目添加到集合时必须增加计数一次,参数化单元测试说明如何向集合中添加“n”个项目。然后,Pex会尝试创建一个单元测试来证明合同被打破。请参见此视频进行概述。

如果成功,您只需要为每个要测试的事物编写一个示例的单元测试,而PEX将能够找出将使测试失败的数据项。


0

1
孤立的链接被认为是一个不好的答案,因为它本身是没有意义的,并且目标资源未来也不能保证存在。最好在这里包含答案的关键部分,并提供参考链接。 - j0k

0

我发现DbC对于启动红绿重构循环非常有用,因为它有助于识别要开始的单元测试。使用DbC,我开始考虑被TDD处理的对象必须处理的前置条件,每个前置条件可能代表一个失败的单元测试来开始红绿重构循环。在某些时候,我会切换到使用一个失败的单元测试来开始一个后置条件的循环,然后继续进行TDD流程。我已经尝试过这种方法与TDD新手一起使用,它确实可以启动TDD思维方式。

总之,将DbC视为识别关键行为单元测试的有效方法。 DbC有助于分析输入(前置条件)和输出(后置条件),这是我们需要控制(输入)和观察(输出)以编写可测试软件(TDD的类似目标)的两个方面。


0

当你使用TDD来实现一个新方法时,你需要一些输入:你需要知道在测试中要检查的断言。设计契约为你提供了这些断言:它们是该方法的后置条件和不变量。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接