TDD - 什么时候可以写一个非失败的测试?

时间:2008-12-11 20:55:17

标签: tdd failing-tests

根据我的理解,在TDD中你必须首先编写一个失败的测试,然后编写代码使其通过,然后重构。但是,如果您的代码已经考虑了您要测试的情况怎么办?

例如,假设我是TDD的排序算法(这只是假设)。我可能会为几个案例编写单元测试:

输入= 1,2,3 输出= 1,2,3

输入= 4,1,3,2 输出= 1,2,3,4
等等...

为了让测试通过,我最终使用了一个快速的肮脏的泡泡排序。然后我重构并用更有效的合并排序算法替换它。后来,我意识到我们需要它是一个稳定的类型,所以我也为此编写了一个测试。当然,测试永远不会失败,因为merge-sort是一种稳定的排序算法!无论如何,我仍然需要这个测试,因为有人再次重构它以使用不同的,可能不稳定的排序算法。

这是否打破了始终编写失败测试的TDD口头禅?我怀疑有人会建议我浪费时间来实现一个不稳定的排序算法,只是为了测试测试用例,然后重新实现merge-sort。你经常遇到类似的情况,你做了什么?

12 个答案:

答案 0 :(得分:15)

首先编写失败的测试然后让它们运行有两个原因;

首先是检查测试是否实际测试了你所写的内容。首先检查它是否失败,您更改代码以使测试运行,然后检查它是否运行。这看起来很愚蠢,但是我曾经多次在我添加了一个代码测试,这个代码已经运行以后发现我在测试中犯了一个错误,导致它总是运行。

第二个也是最重要的原因是阻止你写太多的测试。测试反映了您的设计,您的设计反映了您的要求和要求的变化。当发生这种情况时,您不希望重写大量测试。一个好的经验法则是让每个测试都失败只有一个原因,并且由于这个原因只有一个测试失败。 TDD尝试通过为每个测试,每个功能以及代码库中的每个更改重复标准的红绿重构周期来强制执行此操作。

但当然规则被打破了。如果您记住为什么首先制定这些规则,您可以灵活处理它们。例如,当您发现测试有多个测试时,您可以将其拆分。实际上,您已经编写了两个您之前未见过的新测试。打破并修复代码以查看新测试失败是一种仔细检查的好方法。

答案 1 :(得分:5)

  

我怀疑有人会建议我浪费   实施不稳定的时间   排序算法只是为了测试   测试用例,然后重新实现   合并排序。你多久来一次   在类似的情况下,做什么   你呢?

让我成为推荐它的人。 :)

所有这些都是在您一方面花费的时间与您减少或减轻的风险以及您获得的理解之间的权衡。

继续假设的例子......

如果“稳定性”是一个重要的属性/功能,并且您没有通过使其失败来“测试”,则可以节省执行该工作的时间,但会产生测试错误并且始终存在的风险绿色。

另一方面,如果您通过破坏功能并观察其失败来“测试测试”,则可以降低测试风险。

而且,通配符是,你可能会获得一些重要的知识。例如,在尝试编写“坏”排序并使测试失败时,您可能会更深入地考虑对要排序的类型的比较约束,并发现您使用“x == y”作为用于排序的等价类谓词但实际上“!(x

所以我说错了'花费额外的时间让它失败,即使这意味着故意破坏系统只是为了在屏幕上得到一个红点',因为虽然这些都是“转移“会花费一些时间成本,每隔一段时间就会为你节省一个巨大的包(例如oops,测试中的错误意味着我从未测试过我系统中最重要的属性,或者哎呀,我们对不等式谓词的整个设计搞砸了。这就像玩彩票一样,但从长远来看,赔率对你有利;每周你花5美元买票,通常你输了,但每三个月就赢一次1000美元的累积奖金。

答案 2 :(得分:4)

首先使测试失败的一大优势是它确保您的测试真正测试您的想法。你的测试中可能会有微小的错误导致它根本没有真正测试任何东西。

例如,我曾经在我们的C ++代码库中看到有人检查过测试:

assertTrue(x = 1);

显然,他们没有编程,因此测试首先失败,因为这根本不会测试任何东西。

答案 3 :(得分:3)

简单TDD规则:您编写可能失败的测试。

如果软件工程告诉了我们什么,那就是你无法预测测试结果。甚至没有失败。事实上,对于我来说,看到已经恰好在现有软件中运行的“新功能请求”是很常见的。这很常见,因为许多新功能是现有业务需求的直接扩展。基础的正交软件设计仍然有效。

即。新功能“列表X必须最多包含10个项目”而不是“最多5个项目”将需要一个新的测试用例。当List X的实际实现允许2 ^ 32项时,测试将通过,但在您运行新测试之前,您肯定不知道。

答案 4 :(得分:2)

硬核TDDers会说你总是需要一个失败的测试来验证一个积极的测试不是误报,但我认为实际上很多开发人员都会跳过失败的测试。

答案 5 :(得分:2)

如果您正在编写新代码,则编写测试,然后编写代码,这意味着第一次始终进行失败测试(因为它针对虚拟接口执行) 。 那么你可以多次重构,在这种情况下你可能不需要编写额外的测试,因为你的测试已经足够了。

但是,您可能希望使用TDD方法维护一些代码;在这种情况下,您首先必须编写测试作为特征测试(根据定义,永远不会失败,因为它们是针对工作接口执行的),然后重构。

答案 6 :(得分:2)

有理由在TDD中编写测试,而不仅仅是“测试优先”开发。

假设您的sort方法除了直接排序操作之外还有其他一些属性,例如:它验证所有输入都是整数。你最初并不依赖于它,它不在规范中,因此没有测试。

稍后,如果您决定利用这种额外的行为,您需要编写一个测试,以便其他任何人和重构者都不会破坏您现在依赖的这种额外行为。

答案 7 :(得分:1)

呃......我把TDD周期读作

  • 首先编写测试,这将失败,因为代码只是一个存根
  • 编写代码以便测试通过
  • 必要时重构

没有义务继续编写失败的测试,第一个失败是因为没有代码可以做任何事情。第一个测试的关键是决定界面!

编辑:似乎对“红绿重构”的口头禅有一些误解。根据{{​​3}}

在测试驱动的开发中,每个新功能都从编写测试开始。此测试必然会失败,因为它是在功能实现之前编写的。

换句话说,必须失败的测试适用于新功能,而非额外的覆盖范围!

编辑:当然,除非您正在讨论编写回归测试以重现错误!

答案 8 :(得分:1)

您提供的示例是IMO编写首次尝试通过的测试的正确时间之一。正确测试的目的是记录系统的 预期行为 。可以在不改变实现的情况下编写测试,以进一步阐明预期的行为。

P.S。

据我所知,这就是你希望测试在通过之前失败的原因:

你“编写一个你知道的测试会失败但在测试通过之前测试”的原因是,每隔一段时间,测试肯定会失败的原始假设是错误的。在这些情况下,测试现在可以避免编写不必要的代码。

答案 9 :(得分:1)

正如其他人所说,TDD的口号是“没有失败的单元测试就没有新代码”。我从未听说任何TDD从业者说“没有丢失代码就没有新的测试”。新的测试总是受欢迎的,即使它们“发生”到“意外”通过。没有必要将代码更改为中断,然后将其更改为通过测试。

答案 10 :(得分:0)

  

但是,如果您的代码已经考虑了您要测试的情况怎么办?

     

这是否会破坏始终编写失败测试的TDD口号?

是的,因为你已经打破了在代码之前编写测试的咒语。您可以删除代码并重新开始,或者只是从一开始就接受测试。

答案 11 :(得分:0)

我多次遇到过这种情况。虽然我建议并尝试使用TDD,但有时它会使流量过多而无法停止并编写测试。

我有一个两步解决方案:

  1. 一旦有了工作代码和非失败的测试,故意在代码中插入一个更改,导致测试失败。
  2. 从原始代码中删除更改并将其放入注释中 - 无论是在代码方法还是在测试方法中,因此,下次有人想确定测试仍然在尝试失败时,他们知道该怎么做。这也可以证明您确认测试失败了。如果它是最干净的,请将其保留在代码方法中。您甚至可能希望使用条件编译来启用测试破坏程序