在编写源代码之前如何编写单元测试?

时间:2013-01-26 14:09:44

标签: unit-testing tdd

由于单元测试是一个白盒测试,它假定您必须事先知道您的代码必须处理的所有情况,您的代码必须处理的所有客户端对象(测试中的Mock对象),以及正确的客户端对象必须出现在代码中的顺序(因为单元测试考虑了模拟对象的调用)。换句话说,您必须确切地知道代码的详细算法。在您完全了解代码的算法之前,您必须先编写代码!

从我的角度来看,我没有看到在编写源代码之前如何编写正确的单元测试。然而,由于功能测试是用户需求的一部分,因此可以先编写功能测试。 你的建议? 最好的关注

为此问题提供了一个例子:
How to write test code before write source code when they are objects dependencies?

5 个答案:

答案 0 :(得分:22)

  

换句话说,您必须确切知道代码的详细算法。

不完全。您必须确切地知道代码的详细行为,如从代码本身外部所观察到的那样。实现此行为的算法,或算法的组合,或任何级别的抽象/嵌套/计算/等。这些测试并不重要。测试只关心达到预期的结果。

然后,测试的价值在于它们是代码应该如何表现的规范。所以代码可以自由地改变你想要的,只要它仍然可以针对测试进行验证。您可以提高性能,重构可读性和可支持性等。测试确保行为保持不变。

例如,假设我想编写一个添加两个数字的函数。你可能会在脑海中知道你将如何实现它,但暂时放下这些知识。你还没有实现它。首先,您正在实施测试...

public void CanAddIntegers()
{
    var addend = 1;
    var augend = 1;
    var result = MyMathObject.Add(addend, augend);
    Assert.AreEqual(2, result);
}

现在你有了一个测试,你可以实现这个方法......

public int Add(int addend, int augend)
{
    return ((addend * 2) + (augend * 2)) / 2;
}

哇。等一下......为什么我在地球上实现它呢?那么,从测试的角度来看,谁在乎呢?它过去了。实施符合要求。现在我有一个测试,我可以安全地重构代码...

public int Add(int addend, int augend)
{
    return addend + augend;
}

这更加明智。测试仍然通过。事实上,我可以进一步减少代码...

public int Add(int addend, int augend)
{
    return 2;
}
猜猜是什么?测试仍然通过。它是我们唯一的测试,它是唯一给出的规范,所以代码"有效。"显然,我们需要改进测试以涵盖更多案例。编写更多测试将为我们提供编写更多代码所需的规范。

事实上,根据the third rule of TDD,最后一个应该应该是第一个实现

  

您不能再编写足以通过一次失败的单元测试的生产代码。

因此,在纯粹的Uncle-Bob驱动的TDD世界中,我们首先编写最后一个实现,然后编写更多测试并逐步改进代码。

这称为红色,绿色,重构循环。它在一个简单的,人为设计的例子Bowling Game中得到了很好的说明。该练习的目的是练习这个循环:

  1. 首先,编写一个期望某些行为的测试。这是循环中的 Red 部分,因为测试将在没有行为的情况下失败。
  2. 接下来,编写代码以展示该行为。这是循环的绿色部分,因为它的目的是让测试通过。 才能通过测试。
  3. 最后,重构代码并改进它。当然,这是循环中 Refactor 的一部分。
  4. 您遇到困难的地方是您永远处于周期的重构部分。您已经在考虑如何使代码更好。什么算法是正确的,如何优化它,最终应该如何写入。为此,TDD是耐心的练习。不要写出最好的代码......

    1. 首先,确定代码应该做什么,然后再
    2. 接下来,编写 它的代码,
    3. 最后,改进代码并使其更好。

    4. <强>更新

      我遇到了一些让我想起这个问题的东西,但我发现了一个随机的东西。也许我误解了你所问的问题。你是如何管理你的依赖关系的?也就是说,你使用什么样的依赖注入方法?听起来这可能是这里讨论的问题的根源。

      就我记忆而言,我已经使用了Common Service Locator之类的东西(或者更常见的是同一概念的本土实现)。在这样做的过程中,我倾向于采用非常特殊的依赖注入方式。听起来你正在使用不同的风格。也许是构造函数注入?为了这个答案,我将假设构造函数注入。

      然后,让我们说,MyMathObject依赖于MyOtherClass1MyOtherClass2。使用构造函数注入,使MyMathObject的覆盖区看起来像这样:

      public class MyMathObject
      {
          public MyMathObject(MyOtherClass1 firstDependency, MyOtherClass2 secondDependency)
          {
              // implementation details
          }
      
          public int Add(int addend, int augend)
          {
              // implementation details
          }
      }
      

      因此,正如您所指出的,测试需要提供其依赖关系或模拟。在课程的足迹中,没有MyOtherClass1MyOtherClass2实际使用的迹象,但有迹象表明需要为他们。作为依赖项,构造函数会大声宣传它们。

      所以这引出了你曾经问过的问题......当一个人还没有实现这个对象时,怎么能先编写测试呢?同样,仅在对象的面向外部的设计中没有实际使用的指示。所以依赖是一个需要知道的实现细节。

      否则,您首先要写下这个:

      public class MyMathObject
      {
          public int Add(int addend, int augend)
          {
              // implementation details
          }
      }
      

      然后你为它编写测试,然后你实现它并发现依赖关系,然后你重新编写你的测试。这就是问题所在。

      但是,您发现的问题不是测试或测试驱动开发的问题。问题实际上是在对象的设计中。尽管// implementation details已经被覆盖,但仍有一个实现细节可以逃脱。有一个漏洞抽象

      public class MyMathObject
      {
          public MyMathObject(MyOtherClass1 firstDependency, MyOtherClass2 secondDependency)
          {                   ^---Right here                 ^---And here
      
              // implementation details
          }
      
          public int Add(int addend, int augend)
          {
              // implementation details
          }
      }
      

      该对象并未充分封装和抽象其实现细节。它正在努力,依赖注入的使用是朝着这一目标迈出的重要一步。但它尚未完全存在。这是因为作为实现细节的依赖项是其他对象外部可见外部已知。 (在这种情况下,测试对象。)因此,为了满足依赖关系并使MyMathObject工作,外部对象需要知道它的实现细节。他们都这样做。测试对象,使用它的任何生产代码对象,以任何方式依赖它的任何东西和所有东西。

      为此,您可能需要考虑切换依赖关系的管理方式。而不是像构造函数注入或setter注入那样,进一步反转依赖关系的管理,让对象在内部通过另一个对象解析它们。

      使用上述服务定位器作为起始模式,制作一个唯一目的(其唯一职责)是解决依赖关系的对象非常容易。如果你正在使用依赖注入框架,那么这个对象通常只是框架功能的传递(但是抽象框架本身......所以减少一个依赖,这是一件好事)。如果使用本地功能而不是此对象抽象出该功能。

      但是你最终得到的是MyMathObject中的类似内容:

      private SomeInternalFunction()
      {
          var firstDependency = ServiceLocatorObject.Resolve<MyOtherClass1>();
          // implementation details
      }
      

      所以现在MyMathObject的足迹,即使是依赖注入,也是:

      public class MyMathObject
      {
          public int Add(int addend, int augend)
          {
              // implementation details
          }
      }
      

      没有漏洞抽象,没有外部已知的依赖关系。随着实施细节的变化,测试不需要更改。这是将测试与他们正在测试的对象分离的另一个步骤。

答案 1 :(得分:1)

显然,如果您不了解“软件”的意图,就无法编写测试,但如果 要求规范,则绝对可以 详细。

你可以写一些符合要求的内容; 然后根据规范产生最少量的工作以使测试通过。

除非你是某种天才 - 否则第一次削减将需要重构和引入抽象,模式,可维护性,性能和各种其他因素。

因此,如果要求被理解,您可以先测试 - 但测试不会通过,直到实现结合在一起,并且只需要执行测试通过。

以这种方式工作并不总是符合现实法案 - 特别是如果很难得到规范的话。如果你没有得到你作为开发人员所需要的东西,你需要小心不要盲目地沿着这条路走下去。在继承代码或添加“棕色地带”项目时,通常也无法实现。作为开发人员,在早期确定实用性非常重要。

答案 2 :(得分:0)

这对于人们在启动TDD时过去是一个非常难的问题。我确信在SO上有很多好的答案,特别是在programmers.stackexchange.com上(这个问题可能更适合那个论坛)。

我可以说很多东西可以帮助你理解,但没有一个能像你实际做一些TDD一样好用。作为一个快速入门的地方,我将向您推荐this article on code katas,它链接到一些有用的TDD练习。

答案 3 :(得分:0)

嗯,不是真的。使用TDD,您将开始使用您期望产品代码处理的测试用例。它并不意味着应该有 no 产品代码。你的类函数等可以存在。您从失败的测试用例开始,然后不断更改产品代码以使其通过。

请参阅http://msdn.microsoft.com/en-us/library/aa730844(v=vs.80).aspx#guidelinesfortdd_topic2

答案 4 :(得分:0)

首先:大量的单元测试不需要Mocks,也不需要与其他对象进行交互;你的问题不适用于那些。

如果新方法的目的或部分目的是对某些协作对象产生影响,那么这就是必须测试的内容的一部分。如果这不是它的目的,但你的实施偶然会对协作者产生影响,那么你就不应该测试那种偶然的影响。

无论哪种方式,很容易看出你可以在编写代码之前编写测试。如果你的方法应该影响另一个对象,那么测试应该这样说。如果不应该使用您的方法,则测试不需要对方法与其他对象的交互作出任何说明。