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

时间:2009-12-26 01:06:15

标签: oop tdd

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

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

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

很棒的答案。很难选出胜利者

8 个答案:

答案 0 :(得分:12)

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

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

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

答案 1 :(得分:9)

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

  1. 如果您在编码之前编写测试,在一个函数中测试的多个行为意味着您一次实现多个行为,这是一个糟糕的因素。
  2. 每个函数测试一个行为意味着如果测试失败,您就会知道完全失败的原因,并且可以将特定问题区域归零。如果您在单个函数中测试了多个行为,则“稍后”测试中的失败可能是由于早期测试中未报告的失​​败导致了错误状态。
  3. 每个函数测试一个行为意味着如果需要重新定义该行为,您只需要担心特定于该行为的测试,而不必担心其他不相关的测试(好吧,至少不是由于测试布局...)
  4. 最后一个问题 - 为什么每个功能都有一个测试?有什么好处?我不认为对功能声明征税。

答案 2 :(得分:6)

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

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

答案 3 :(得分: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

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

  

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

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

 DoSomethingTest_TestWhenFooIsTrue_RequestIsMadeToWebServiceA()
 DoSomethingTest_TestWhenFooIsFalse_RequestIsMadeToWebServiceB()

此外 - 这只是一般指导原则。肯定会出现在同一单元测试中有多个断言的情况。当您测试相同的控制流时会发生这种情况,但是在编写断言语句时需要检查多个字段。以此为例 - 对函数进行测试,该函数将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

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

-DREW

答案 4 :(得分:3)

当测试功能仅执行一次测试时,更容易识别哪种情况失败。

您还要隔离测试,因此一个测试失败不会影响其他测试的执行。

答案 5 :(得分:3)

我认为好的方法是不考虑每个功能的测试数量,而是考虑code coverage

  
      
  • 功能覆盖 - 具有中的每个功能(或子程序)   程序被称为?
  •   
  • 声明范围 - 程序中的每个节点都是
      执行?
  •   
  • 分支覆盖范围 - 程序中的每个边缘都是
      执行?
  •   
  • 决策覆盖范围 - 具有每个控制结构(例如IF   声明)评估为真和   假?
  •   
  • 条件覆盖 - 评估每个布尔子表达式   既真实又虚假?事实并非如此   必然意味着决策覆盖。
  •   
  • 条件/决定覆盖范围 - 决定和条件覆盖范围
      应该满意。
  •   

编辑: 我重读了我写的内容,发现它有点“可怕”......这让我想起了一个很好的想法我heard几个星期左右的代码覆盖率:

  

代码覆盖就像股市一样   投资!你需要投入足够的资金   是时候有一个良好的报道但不是   太多了,不要浪费你的时间和打击   你的项目!

答案 6 :(得分:2)

似乎多测试功能中的单个故障必须导致所有人失败,对吧?通常测试框架测试只是通过失败,使用多测试方法意味着您必须手动确定哪些多个测试将失败,因为如果您运行了大量测试,则第一次执行失败将导致功能整体失败,进一步的测试不会失败。

测试中的粒度很好。如果您要编写5个测试,那么将它们各自放在自己的函数中似乎并不比将它们全部放在同一位置更困难,除了每次创建新的样板函数的微小开销。使用正确的IDE,即使这可能比复制&粘贴。

答案 7 :(得分:2)

考虑这个稻草人(在C#中)

void FooTest()
{
    C c = new C();
    c.Foo();
    Assert(c.X == 7);
    Assert(c.Y == -7);
}

虽然“每个测试功能一个断言”是很好的TDD建议,但它不完整。单独应用它会给出:

void FooTestX()
{
    C c = new C();
    c.Foo();
    Assert(c.X == 7);
}

void FooTestY()
{
    C c = new C();
    c.Foo();
    Assert(c.X == 7);
}

缺少两件事:一次一次(又称DRY),“每个场景一个测试类”。后者是鲜为人知的:而不是一个包含所有测试方法的测试类/测试夹具,具有非平凡场景的嵌套类。像这样:

class CTests
{
    class FooTests
    {
        readonly C c;

        void Setup()
        {
            c = new C();
            c.Foo();
        }

        void XTest()
        {
            Assert(c.X == 7);
        }

        void YTest()
        {
            Assert(c.Y == -7);
        }
    }
}

现在你没有重复,并且每个测试方法都断言有关被测代码的一件事。

如果它不是那么冗长,我会考虑用这种方式编写所有测试,这样测试方法总是只需要一个断言的简单单行方法。但是,当测试不与另一个测试共享“设置”代码时,它似乎太笨拙了。

(我已经避免了特定于单元测试技术的细节,例如NUnit或MSTest。你必须调整以适应你正在使用的任何东西,但原则是合理的。)