8

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

例如,假设我正在对排序算法进行 TDD(这只是假设)。我可能会为几种情况编写单元测试:

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

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

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

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

4

12 回答 12

16

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

首先是检查测试是否真的在测试你写它的目的。您首先检查它是否失败,更改代码以使测试运行,然后检查它是否运行。这看起来很愚蠢,但我有几次为已经运行的代码添加了一个测试,后来发现我在测试中犯了一个错误,使它始终运行。

第二个也是最重要的原因是防止您编写过多的测试。测试反映您的设计,您的设计反映您的需求和需求变化。发生这种情况时,您不想重写大量测试。一个好的经验法则是让每个测试只因为一个原因而失败,并且只有一个测试因为这个原因而失败。TDD 试图通过为每个测试、每个特性和代码库中的每个更改重复标准的红绿重构循环来强制执行此操作。

但当然,规则是用来打破的。如果您牢记为什么首先制定这些规则,您可以灵活地使用它们。例如,当您发现您有测试不止一件事的测试时,您可以将其拆分。实际上,您已经编写了两个以前从未失败过的新测试。打破然后修复你的代码以查看你的新测试失败是仔细检查事情的好方法。

于 2008-12-11T21:07:37.243 回答
5

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

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

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

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

如果“稳定性”是一个重要的属性/特性,并且您不会通过使其失败来“测试测试”,那么您可以节省做这项工作的时间,但会承担测试错误并且永远是绿色的风险。

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

而且,通配符是,您可能会获得一些重要的知识。例如,在尝试编写“坏”排序并导致测试失败时,您可能会更深入地考虑您正在排序的类型的比较约束,并发现您使用“x==y”作为equivalence-class-predicate 用于排序,但实际上 "!(x<y) && !(y<x)" 对您的系统来说是更好的谓词(例如,您可能会发现错误或设计缺陷)。

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

于 2008-12-15T20:10:55.267 回答
4

首先让测试失败的一大优势是,它可以确保你的测试真的是在测试你的想法。您的测试中可能存在细微的错误,导致它根本无法真正测试任何东西。

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

assertTrue(x = 1);

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

于 2008-12-11T21:00:01.097 回答
3

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

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

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

于 2008-12-15T10:28:04.430 回答
2

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

于 2008-12-11T21:00:49.953 回答
2

如果您正在编写一段新代码,则编写测试,然后编写代码,这意味着您第一次总是有一个失败的测试(因为它是针对一个虚拟接口执行的)。然后你可能会重构几次,在这种情况下你可能不需要编写额外的测试,因为你拥有的可能已经足够了。

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

于 2008-12-11T21:00:58.410 回答
2

除了“测试优先”开发之外,还有理由在 TDD 中编写测试。

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

稍后,如果您决定利用此附加行为,则需要编写一个测试,以便其他任何出现并进行重构的人都不会破坏您现在依赖的此附加行为。

于 2008-12-11T21:26:05.160 回答
1

呃...我将 TDD 周期读为

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

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

编辑:似乎对“红绿重构”的口头禅有些误解。根据维基百科的 TDD 文章

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

换句话说,必须失败的测试是针对新功能,而不是针对额外的覆盖范围!

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

于 2008-12-11T21:00:23.980 回答
1

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

这是否打破了始终编写失败测试的 TDD 口头禅?

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

于 2008-12-11T21:36:33.360 回答
1

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

附言

据我了解,这就是您希望测试在通过之前失败的原因:

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

于 2009-01-11T18:29:00.503 回答
1

正如其他人所说,TDD 的口号是“没有失败的单元测试就没有新代码”。我从未听过任何 TDD 从业者说“没有缺少代码就没有新测试”。新的测试总是受欢迎的,即使它们“碰巧”“意外”通过。无需更改代码即可中断,然后将其更改回以通过测试。

于 2009-01-26T05:59:36.853 回答
0

我已经多次遇到这种情况。虽然我推荐并尝试使用 TDD,但有时它会过多地破坏流程以停止和编写测试。

我有一个两步解决方案:

  1. 一旦你有你的工作代码和你的非失败测试,​​故意在代码中插入一个更改以导致测试失败。
  2. 从您的原始代码中删除该更改并将其放在注释中 - 无论是在代码方法中还是在测试方法中,这样下次有人想确定测试仍然出现故障时,他们知道该怎么做。这也可以作为您确认测试失败的证据。如果它最干净,请将其留在代码方法中。您甚至可能想要使用条件编译来启用测试断路器
于 2009-01-26T04:48:41.193 回答