TDD:为什么每个功能只有一个测试?

Joh*_*ker 10 oop tdd

我很难理解为什么在我看到的大多数专业TDD代码中每个功能只有一个测试.当我最初接触TDD时,如果它们相关,我倾向于对每个功能进行4-5次测试,但我认为这似乎不是标准.我知道每个函数只进行一次测试更具描述性,因为你可以更容易地缩小问题的范围,但我发现自己很难想出功能名来区分不同的测试,因为许多测试非常相似.

所以我的问题是:在一个函数中放置多个测试真的是一个不好的做法,如果是这样,为什么呢?那里有共识吗?谢谢

编辑:哇哇大的答案.我相信.你需要把它们全部分开.我经历了最近的一些测试,并将它们全部分开,并且看起来更容易阅读并帮助我更好地理解我正在测试的内容.同样通过给测试他们自己冗长的冗长名称,它给了我一些想法,比如"哦等我没有测试这个其他的东西",所以我认为这是我要走的路.

很棒的答案.要赢得冠军很难

jus*_*ody 12

看起来你问"为什么在我看过的大多数专业TDD代码中每次测试只有一个断言 ".这可能会增加测试隔离度,以及出现故障时的测试覆盖率.这就是我用这种方式创建TDD库(用于PHP)的原因.说你有

function testFoo()
{
    $this->assertEquals(1, foo(10));
    $this->assertEquals(2, foo(20));
    $this->assertEquals(3, foo(30));
}
Run Code Online (Sandbox Code Playgroud)

如果第一个断言失败,你就不会看到其他两个断言会发生什么.这并不完全有助于查明问题:这是输入特有的,还是系统性的?

  • @John Baker:是的,单断言单元测试的黑暗面是膨胀的.但是在我进入TDD业务后很快就很清楚,在实现语言中编写单元测试是一个错误,这个业务*真的*需要一个解耦的DSL,你可以在其中编写多断言测试并保持单断言测试的"我想看到所有失败"属性. (3认同)

kyo*_*ryu 9

是的,您应该在TDD中测试每个功能的一个行为.这就是原因.

  1. 如果您在编码之前编写测试,则在一个函数中测试的多个行为意味着您一次实现多个行为,这是一个糟糕的因素.
  2. 每个函数测试一个行为意味着如果测试失败,您就会确切地知道它失败的原因,并且可以将特定问题区域归零.如果您在单个函数中测试了多个行为,则"稍后"测试中的失败可能是由于先前测试中未报告的导致错误状态的失败.
  3. 每个函数测试一个行为意味着如果需要重新定义该行为,您只需要担心特定于该行为的测试,而不必担心其他不相关的测试(好吧,至少不是由于测试布局... .)

而且,最后一个问题-为什么不能有每个功能的一个测试?有什么好处?我不认为对功能声明征税.


Ale*_*lli 6

建议进行高粒度的测试,不仅仅是为了便于识别问题,而且因为函数内部的排序测试可能会意外地隐藏问题.假设例如foo带有参数的调用方法bar应该返回23- 但是由于对象初始化其状态的方式中的错误,42如果它被调用为新构造对象的第一个方法(之后,它会返回)正确切换到返回23).如果您的测试foo在对象创建后没有出现,那么您将会错过这个问题; 如果你一次测试5个,那么你只有20%的几率意外地做到了.每个功能进行一次测试(以及每次都干净地重置和重建所有内容的设置/拆卸安排),您将立即修复该错误.现在这只是一个人为简单的问题,仅仅是出于讨论的原因,但是一般的问题 - 测试不应该相互影响,但往往除非它们每个都被设置和拆除功能所包围 - 确实很大.

是的,将事情命名(包括测试)并不是一个小问题,但不能以此为借口来避免适当的粒度.一个有用的命名提示:每个测试都会检查给定的特定行为 - 例如,"2008年的复活节是在3月23日" - 而不是通用的"功能",例如"正确计算复活节日期".

  • +1保持测试简单是一个黄金的建议.你想要的最后一件事是测试中的一个错误. (3认同)

dfe*_*aro 6

我很难理解为什么在我看到的大多数专业TDD代码中每个功能只有一个测试

当你说'测试'时,我假设你的意思是'断言'.通常,测试应该只测试函数的单个"用例"."用例"是指:代码可以通过控制流语句流经的路径(不要忘记处理的异常等).基本上,您正在测试该功能的所有"要求".例如,假设你有一个如下功能:

Public Function DoSomething(ByVal foo as Boolean) As Integer
   Dim result as integer = 0     

   If(foo) then
        result = MakeRequestToWebServiceA()
   Else
        result = MakeRequestToWebServiceB()
   End If     

   return result
End Function
Run Code Online (Sandbox Code Playgroud)

在这种情况下,函数可以使用2个"用例"或控制流.此功能至少应有2次测试.一个接受foo为true并将if(true)代码分支的一个,以及一个接受foo为false并向下移动到第二个分支的代码.如果你有更多的if语句或流程代码可以通过,那么它将需要更多的测试.这有几个原因 - 对我来说最重要的是没有它,测试会太复杂,难以阅读.还有其他原因,例如在上述函数的情况下,控制流程基于输入参数 - 这意味着您必须调用该函数两次以测试所有代码路径.一旦您在测试IMO中进行测试,就不应该再调用该函数.

但我发现自己很难想出功能名称来区分不同的测试,因为许多测试非常相似

也许你过度思考了?不要害怕为你的测试功能编写疯狂的,过于冗长的名字.无论测试做什么,用英文写,使用下划线,并为名称提出一套标准,以便其他人查看代码(包括你自己6个月后)可以轻松地弄清楚它的作用.记住,你实际上不必自己调用这个函数(至少在大多数测试框架中),所以谁在乎它的名字是100个字符.疯了.在上面的例子中,我的2个测试将命名为:

 DoSomethingTest_TestWhenFooIsTrue_RequestIsMadeToWebServiceA()
 DoSomethingTest_TestWhenFooIsFalse_RequestIsMadeToWebServiceB()
Run Code Online (Sandbox Code Playgroud)

此外 - 这只是一般指导原则.肯定会出现在同一单元测试中有多个断言的情况.当您测试相同的控制流时会发生这种情况,但是在编写断言语句时需要检查多个字段.以此为例 - 对函数进行测试,该函数将CSV文件解析为具有Header,Body和Footer字段的业务对象:

 Public Sub ParseFileTest_TestFileIsParsedCorrectly()
        Dim target as new FileParser()
        Dim actual as SomeBusinessObject = target.ParseFile(TestHelper.GetTestData("ParseFileTest.csv")))

        Assert.Equals(actual.Header,"EXPECTED HEADER FROM TEST DATA FILE")
        Assert.Equals(actual.Footer,"EXPECTED FOOTER FROM TEST DATA FILE")
        Assert.Equals(actual.Body,"TEST DATA BODY")
 End Sub
Run Code Online (Sandbox Code Playgroud)

在这里,我们确实测试了相同的用例,但我们需要多个断言来检查所有数据并确保我们的代码实际工作.

-DREW