在TDD中,如果我编写的测试用例在不修改生产代码的情况下通过了,那意味着什么?


17

这些是Robert C. Martin的TDD规则

  • 除非要通过失败的单元测试,否则不允许编写任何生产代码。
  • 您不得编写任何足以使单元测试失败的单元测试。编译失败就是失败。
  • 您不能编写任何足以通过一项失败的单元测试的生产代码。

当我编写一个看似值得但未更改生产代码的测试通过时:

  1. 这是否表示我做错了?
  2. 如果可以帮助,将来是否应该避免编写此类测试?
  3. 我应该将该测试留在那里还是将其删除?

注意: 我在这里试图问这个问题:我可以从通过单元测试开始吗? 但是直到现在我还不能很好地阐明这个问题。


您引用的文章中链接到的“保龄球游戏卡塔”实际上具有一个立即通过的测试作为最后一步。
jscs

Answers:


21

它说除非编写通过失败的单元测试,否则您不能编写生产代码,而不是说您不能编写从一开始就通过的测试。该规则的目的是说“如果需要编辑生产代码,请确保首先编写或更改它的测试。”

有时我们编写测试来证明理论。测试通过了,这反驳了我们的理论。然后,我们不会删除测试。但是,我们可能(知道我们有源代码控制的支持)中断了生产代码,以确保我们了解为什么不希望它通过时,为什么会通过。

如果事实证明它是有效且正确的测试,并且没有与现有测试重复,则将其保留在那里。


改善现有代码的测试覆盖率是编写(希望)通过测试的另一个完全有效的理由。
杰克

13

这意味着:

  1. 您编写的产品代码可以实现所需的功能,而无需先编写测试(违反“宗教TDD”),或者
  2. 生产代码已经满足了您需要的功能,而您只是在编写另一个单元测试来涵盖该功能。

后者的情况比您想象的更普遍。作为一个完全似是而非的(但仍然是说明性的)示例,假设您编写了以下单元测试(伪代码,因为我很懒):

public void TestAddMethod()
{
    Assert.IsTrue(Add(2,3) == 5);
}

因为您真正需要的只是2和3相加的结果。

您的实现方法是:

public int add(int x, int y)
{
    return x + y;
}

但是,假设我现在需要将4和6加在一起:

public void TestAddMethod2()
{
    Assert.IsTrue(Add(4,6) == 10);
}

我不需要重写我的方法,因为它已经涵盖了第二种情况。

现在让我们说我发现我的Add函数确实需要返回一个有一定上限的数字,比如说100。我可以编写一个测试此方法的新方法:

public void TestAddMethod3()
{
    Assert.IsTrue(Add(100,100) == 100);
}

并且此测试现在将失败。我现在必须重写我的函数

public int add(int x, int y)
{
    var a = x + y;
    return a > 100 ? 100 : a;
}

使它通过。

常识表明,如果

public void TestAddMethod2()
{
    Assert.IsTrue(Add(4,6) == 10);
}

通过,您不会故意使方法失败,只是为了使测试失败,然后编写新代码以使该测试通过。


5
如果您完全遵循Martin的示例(他不一定建议您这样做),add(2,3)则要通过测试,您实际上会返回5。硬编码。然后,您将编写测试,add(4,6)该测试将迫使您编写生产代码,以使其通过而不会同时断裂add(2,3)。你会最终return x + y,但你不会开始使用它。理论上。自然,马丁(或者我不记得它可能是其他人)喜欢提供这样的教育示例,但是并不希望您实际上以这种方式编写如此琐碎的代码。
安东尼·佩格拉姆

1
@tieTYT,通常,如果我正确地从Martin的书中回想起,第二个测试用例通常足以让您编写一种简单方法的通用解决方案(实际上,您确实会使其工作正常)。第一次)。不需要三分之一。
安东尼·佩格拉姆

2
@tieTYT,然后您将继续编写测试,直到完成为止。:)
安东尼·佩格拉姆

4
还有第三种可能性,它与您的示例背道而驰:您编写了重复测试。如果您“虔诚”地遵循TDD,则通过的新测试始终是一个危险信号。在DRY之后,您永远不要编写两个测试本质上是同一件事的测试。
congusbongus

1
“如果您完全遵循Martin的示例(他不一定建议您这样做),要使add(2,3)通过,您将立即返回5。硬编码。” -这是严格的TDD始终引起我的关注,这种想法是您编写自己知道的代码出于对未来测试和证明的错误的期望。如果出于某种原因从未编写该将来的测试,而同事认为“绿色的所有测试”意味着“所有代码正确的”怎么办?
朱莉娅·海沃德

2

您的测试通过了,但是您没错。我认为,发生这种情况是因为生产代码从一开始就不是TDD。

让我们假设canonical(?)TDD。没有生产代码,但有一些测试用例(当然总会失败)。我们添加生产代码以通过。然后停止此处以添加更多的失败测试用例。再次添加生产代码以通过。

换句话说,您的测试可能是一种功能测试,而不是简单的TDD单元测试。这些始终是保证产品质量的宝贵资产。

我个人不喜欢这种极权主义,不人道的规则;(


2

实际上,昨晚在道场上也遇到了同样的问题。

我对此进行了快速研究。这是我想出的:

基本上,TDD规则并未明确禁止它。也许需要一些额外的测试来证明一个函数对于通用输入正确地起作用。在这种情况下,TDD练习将被搁置一会儿。请注意,只要在此期间内未添加任何生产代码,就可以暂时退出TDD练习并不一定会违反TDD规则。

可以编写其他测试,只要它们不是多余的即可。一个好的做法是进行等效类划分测试。这意味着将测试每个等效类的边缘情况和至少一个内部情况。

但是,这种方法可能会出现的一个问题是,如果从一开始就通过了测试,就不能保证没有误报。这意味着可能存在通过测试的原因,因为测试未正确实现,而不是因为生产代码运行正常。为避免这种情况,应稍微更改生产代码以打破测试。如果这使测试失败,则很可能正确地执行了测试,并且可以将生产代码改回以使测试再次通过。

如果您只想练习严格的TDD,则可能不会编写从头开始通过的任何其他测试。另一方面,在企业开发环境中,如果其他测试似乎有用,则实际上应该离开TDD实践。


0

在不修改生产代码的情况下通过的测试并不是天生就糟糕的,通常对于描述其他需求或边界情况很有必要。只要您的测试“值得”,就可以坚持下去。

遇到麻烦的地方是编写一个已经通过的测试来代替实际理解问题空间的测试。

我们可以想像有两个极端:一个程序员编写大量测试以防万一,一个程序发现一个错误;第二位程序员在编写最少数量的测试之前仔细分析问题空间。假设两者都在尝试实现绝对值函数。

第一个程序员写道:

assert abs(-88888) == 88888
assert abs(-12345) == 12345
assert abs(-5000) == 5000
assert abs(-32) == 32
assert abs(46) == 46
assert abs(50) == 50
assert abs(5001) == 5001
assert abs(999999) == 999999
...

第二位程序员写道:

assert abs(-1) == 1
assert abs(0) == 0
assert abs(1) == 1

第一个程序员的实现可能导致:

def abs(n):
    if n < 0:
        return -n
    elif n > 0:
        return n

第二个程序员的实现可能会导致:

def abs(n):
    if n < 0:
        return -n
    else:
        return n

所有测试都通过了,但是第一个程序员不仅编写了多个冗余测试(不必要地减慢了开发周期),而且还没有测试边界情况(abs(0))。

如果您发现自己编写的测试在不修改生产代码的情况下通过了,请问一下自己,您的测试是否真的在增加价值,或者您是否需要花费更多的时间来理解问题空间。


好吧,第二个程序员显然也对测试不小心,因为他的同事重新定义abs(n) = n*n并通过了测试。
Eiko

@Eiko你是绝对正确的。编写过一些测试,可以很严重咬你。第二位程序员至少没有进行测试就过于testing abs(-2)。与一切一样,节制是关键。
thinkterry
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.