编写实现后如何纠正测试中的错误


21

如果正确实施逻辑后,测试仍然失败(因为测试中有错误),则TDD中的最佳措施是什么?

例如,假设您要开发以下功能:

int add(int a, int b) {
    return a + b;
}

假设我们按照以下步骤进行开发:

  1. 编写测试(尚无功能):

    // test1
    Assert.assertEquals(5, add(2, 3));
    

    导致编译错误。

  2. 编写一个虚拟函数实现:

    int add(int a, int b) {
        return 5;
    }
    

    结果:test1通过。

  3. 添加另一个测试用例:

    // test2 -- notice the wrong expected value (should be 11)!
    Assert.assertEquals(12, add(5, 6));
    

    结果:test2失败,test1仍然通过。

  4. 编写实际的实现:

    int add(int a, int b) {
        return a + b;
    }
    

    结果:test1仍然通过,test2仍然失败(因为11 != 12)。

在这种特定情况下:

  1. 更正test2,看看它现在通过了,或者
  2. 删除实现的新部分(即返回上面的步骤2),更正test2并使其失败,然后重新引入正确的实现(上面的步骤4)。

还是还有其他更聪明的方法?

虽然我知道示例问题相当琐碎,但我对通用情况下的操作很感兴趣,这可能比两个数的加法复杂。

编辑(响应@Thomas Junk的回答):

这个问题的重点是在这种情况下TDD提出的建议,而不是实现良好代码或测试(可能与TDD方式不同)的“通用最佳实践”。


3
相对于红条进行重构是一个相关的概念。
RubberDuck

5
显然,您需要在TDD上执行TDD。
Blrfl

17
如果有人问我为什么对TDD持怀疑态度,我会向他们指出这个问题。这是卡夫卡斯克风格。
Traubenfuchs

@Blrfl这就是Xibit告诉我们的»我把TDD放在TDD中,这样您就可以在TDDing的同时TDD«:D
Thomas Junk

3
@Traubenfuchs我承认问题乍一看似乎很愚蠢,我也不主张“一直做TDD”,但我确实相信看到测试失败,然后编写使测试通过的代码有很大的好处。 (毕竟,这个问题实际上是关于什么的)。
文森特·萨瓦德

Answers:


31

绝对关键的是,您会看到测试通过和失败。

删除代码以使测试失败然后重写代码还是将其潜入剪贴板,然后再粘贴回去都无所谓。TDD从未说过您必须重新输入任何内容。它想知道测试仅在应通过时通过,而仅在应通过时失败。

看到测试通过和失败都是您测试测试的方式。永远不要相信您从未见过的一项测试。


针对红条进行重构为我们提供了重构工作测试的正式步骤:

  • 运行测试
    • 注意绿色栏
    • 破坏正在测试的代码
  • 运行测试
    • 注意红色条
    • 重构测试
  • 运行测试
    • 注意红色条
    • 破解正在测试的代码
  • 运行测试
    • 注意绿色栏

但是,我们不会重构工作测试。我们必须改造一个越野车测试。一个令人关注的问题是,仅在此测试涵盖的范围内引入了代码。修复测试后,应回滚并重新引入此类代码。

如果不是这种情况,并且由于其他测试覆盖了代码,那么代码覆盖率也不是问题,您可以转换该测试并将其引入为绿色测试。

在这里,代码也被回滚,但是刚好足以导致测试失败。如果这还不足以涵盖所有引入的代码,而仅由越野车测试覆盖,则我们需要更大的代码回滚和更多测试。

进行绿色测试

  • 运行测试
    • 注意绿色栏
    • 破坏正在测试的代码
  • 运行测试
    • 注意红色条
    • 破解正在测试的代码
  • 运行测试
    • 注意绿色栏

破坏代码可以是注释掉代码,也可以将其移到其他位置以供稍后粘贴回去。这向我们展示了测试涵盖的代码范围。

对于这最后的两次运行,您将回到正常的红色绿色循环。您只是粘贴而不是键入内容来破坏代码并通过测试。因此,请确保您仅粘贴了足以使测试通过的内容。

这里的总体模式是查看测试的颜色改变我们期望的方式。请注意,这会造成您短暂进行不受信任的绿色测试的情况。请小心不要被打扰,并忘记您在这些步骤中的位置。

感谢RubberDuck的“ 拥抱红色条形”链接。


2
我最喜欢这个答案:看到测试失败并输入错误的代码很重要,因此我将删除/注释代码,更正测试,然后查看失败,然后放回代码(也许会引入故意的错误以将测试放入测试),并进行正确编码以使其正常工作。完全删除并重写它是XP,但是有时您只需要务实。;)
GolezTrol

@GolezTrol我想我的回答是一样的,因此,如果您对此不清楚,请多多指教。
jonrsharpe

@jonrsharpe您的回答也很好,我什至在读完这篇文章之前就投票了。但是在您非常严格地还原代码的地方,CandiedOrange建议了一种更务实的方法,它对我更具吸引力。
GolezTrol

@GolezTrol我没有说如何还原代码;将其注释掉,剪切和粘贴,存储,使用IDE的历史记录;没关系。至关重要的是为什么要这样做:因此您可以检查是否遇到了正确的故障。我已经编辑过,希望能澄清一下。
jonrsharpe

10

您要实现的总体目标是什么?

  • 做好的测试?

  • 进行正确的实施?

  • 虔诚地做TTD 吗?

  • 以上都不是?

也许您过分考虑了与测试和测试之间的关系。

测试不能保证实现的正确性。通过所有测试后,您的软件是否执行应做的事情就什么也没有。它对您的软件不做任何实质性陈述。

以您的示例为例:

添加的“正确”实现将是等效于的代码a+b。而且只要您的代码能够做到,您就可以说算法是正确的,并且可以正确实现。

int add(int a, int b) {
    return a + b;
}

第一眼,我们都同意,这一个另外的实施。

但是我们实际上并不是在说,这段代码只是addition它的实现,在某种程度上表现得像一个:想想整数溢出

整数溢出确实会在代码中发生,但不会在中出现addition。因此:您的代码在某种程度上的行为类似于的概念addition,但不是addition

这种颇为哲学的观点具有若干后果。

一个就是,您可以说,测试不过是对代码预期行为的假设。在测试代​​码时,您可能(也许)永远无法确保自己的实现正确的,最好的说就是,您对代码交付的结果的期望是否得到满足;是,您的代码是错误的,还是您的测试是错误的,还是它们都是错误的。

有用的测试可以帮助您确定对代码应该做什么的期望:只要我不改变期望,并且只要修改后的代码能够为我带来期望的结果,就可以肯定,我所做的假设结果似乎可以解决。

当您做出错误的假设时,这无济于事。但是,嘿!至少它可以预防精神分裂症:当没有结果时会期待不同的结果。


tl; dr

如果正确实施逻辑后,测试仍然失败(因为测试中有错误),则TDD中的最佳措施是什么?

您的测试是关于代码行为的假设。如果您有充分的理由认为自己的实现是正确的,请修复测试并查看该假设是否成立。


1
我认为有关总体目标的问题非常重要,谢谢提出。对我来说,最高的优先级如下:1.正确的实施方式2.“不错的”测试(或者,我宁愿说“有用的” /“设计良好的”测试)。我认为TDD是实现这两个目标的可能工具。因此,尽管我不想一定要虔诚地遵循TDD,但在这个问题的背景下,我对TDD的观点最感兴趣。我将编辑问题以澄清这一点。
Attilio

因此,您会编写一个测试以检测溢出并在发生时通过的测试,还是会因为算法是加法且溢出产生错误的答案而使测试失败?
杰里·耶利米

1
@JerryJeremiah我的观点是:您的测试应涵盖的内容取决于您的用例。对于您将一堆数字加起来的用例,该算法足够好。如果您知道,您很可能加了“大数字”,那datatype显然是错误的选择。一个测试将显示:您的期望是“为大数目工作”,并且在某些情况下没有得到满足。那么问题是如何处理这些案件。他们是极端案例吗?是的时候,如何处理?也许一些quard子句有助于防止更大的混乱。答案是上下文相关的。
Thomas Junk

7

您需要知道,如果实现错误,则测试将失败,这与实现正确的情况与通过测试不同。因此,您应该更正测试之前将代码放回预期失败的状态,并确保其失败是由于您期望的原因(例如5 != 12),而不是您未曾预料到的其他原因。


我们如何才能检查出由于我们期望的原因而导致的测试失败?
Basilevs

2
@Basilevs:1.对失败的原因应该做一个假设;2.运行测试;3.读取结果失败消息并进行比较。有时,这也暗示了您可以重写测试以给您带来更有意义的错误的方法(例如,即使它们都在测试相同的东西,assertTrue(5 == add(2, 3))输出的结果assertEqual(5, add(2, 3))也不如它们有用)。
jonrsharpe

尚不清楚如何在此应用该原理。我有一个假设-测试返回一个常数,如何再次运行相同的测试将确保我是对的?显然要测试,我需要另一个测试。我建议添加明确的示例来回答。
Basilevs

1
@Basilevs什么?您在步骤3的假设是“测试失败,因为5不等于12”。运行测试将向您显示是由于该原因(在这种情况下继续进行)还是由于某种其他原因(在这种情况下)找出原因而导致测试失败。也许这是语言问题,但我不清楚您的建议。
jonrsharpe

5

在这种特殊情况下,如果您将12更改为11,并且现在测试通过了,我认为您已经很好地测试了测试以及实现,因此不需要太多麻烦。

但是,在更复杂的情况下(例如,您的设置代码有误)也会出现相同的问题。在这种情况下,修复测试后,您应该尝试以某种方式使实现变异,以使特定测试失败,然后还原变异。如果还原实现是最简单的方法,那很好。在您的示例中,您可能会a + b更改为a + aa * b

另外,如果您可以稍微更改断言并看到测试失败,则可以非常有效地测试测试。


0

我会说,这是您最喜欢的版本控制系统的一种情况:

  1. 进行测试的更正,将代码更改保留在工作目录中。
    提交相应的消息Fixed test ... to expect correct output

    使用git,这可能需要使用git add -p测试和实现是否在同一个文件中,否则显然您可以将两个文件分别暂存。

  2. 提交实现代码。

  3. 及时返回以测试步骤1中所做的提交,确保测试实际上失败了

您会看到,这样一来,您在测试失败的测试时就不会依赖编辑能力来将实现代码移开。您可以使用VCS来保存您的工作,并确保VCS记录的历史记录正确地包括失败测试和通过测试。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.