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

Unit testing 如何在编写源代码之前编写单元测试?,unit-testing,tdd,Unit Testing,Tdd,由于单元测试是白盒测试,它假定您必须提前知道代码必须处理的所有情况、代码必须处理的所有客户端对象(在测试中称为模拟对象)以及客户端对象必须在代码中出现的正确顺序(因为单元测试考虑了模拟对象的调用)。换句话说,您必须确切地知道代码的详细算法。在你能确切地知道你的代码的算法之前,你必须先写它 从我的观点来看,我不认为在编写源代码之前编写正确的单元测试是可能的。尽管如此,还是可以先编写功能测试,因为功能测试是一种用户需求。 你的建议? 致意 这个问题给出了一个例子: 显然,如果您不了解“软件”的意图,

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

从我的观点来看,我不认为在编写源代码之前编写正确的单元测试是可能的。尽管如此,还是可以先编写功能测试,因为功能测试是一种用户需求。 你的建议? 致意

这个问题给出了一个例子:

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

你可以写一些符合要求的东西,但会失败;然后根据规范进行最低工作量的测试,使测试通过

除非你是个天才,否则这第一步需要重构和引入抽象,模式、可维护性、性能和所有其他方面都需要考虑

因此,如果理解了需求,您可以先进行测试,但是测试将不会通过,直到实现结合在一起,并且只需要使测试通过的实现


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

这对于启动TDD的人来说是一个很难克服的问题。我相信在SO上有很多很好的答案,尤其是在programmers.stackexchange.com上(这个问题可能更适合那个论坛)


我可以说很多东西来帮助你理解,但是没有一件能像你实际做TDD那样有效。作为一个快速的开始,我将向您介绍一些有用的TDD练习的链接

不太好。有了TDD,您就可以从预期产品代码将要处理的测试用例开始。这并不意味着不应该有产品代码。您的类、函数等可以存在。您从失败的测试用例开始,然后继续更改产品代码以使其通过

换句话说,您必须确切了解代码的详细算法

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

因此,测试的价值在于它们是代码应该如何运行的规范。因此,只要代码仍然可以根据测试进行验证,代码就可以随意更改。您可以改进性能、重构可读性和可支持性等。测试确保行为保持不变

例如,假设我想写一个函数,它将两个数字相加。你可能在脑子里知道你将如何实现它,但暂时把这些知识放在一边。你还没有实现它。首先,您正在实施测试

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;
}
这有点理智。而且测试还是通过了。事实上,我可以进一步减少代码

public int Add(int addend, int augend)
{
    return addend + augend;
}
public int Add(int addend, int augend)
{
    return 2;
}
你猜怎么着?考试还是通过了。这是我们唯一的测试,它是唯一给出的规范,因此代码“有效”。因此,显然我们需要改进测试以涵盖更多的情况。编写更多的测试将为我们提供编写更多代码所需的规范

事实上,最后一次实施应该是第一次,根据:

不允许您编写的生产代码超过足以通过一个失败的单元测试的数量

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

这就是所谓的红、绿、重构循环。这在一个简单的稍微不那么做作的例子中得到了很好的说明。该练习的目的是练习该循环:

  • 首先,编写一个预期特定行为的测试。这是循环的红色部分,因为如果没有适当的行为,测试将失败
  • 接下来,编写代码来展示这种行为。这是循环的绿色部分,因为它的目的是让测试通过。只是为了通过考试
  • 最后,重构代码并对其进行改进。这自然是周期的重构部分
  • 你陷入困境的地方是,你永远处于重构周期的部分。您已经在考虑如何改进代码。什么算法是正确的,如何优化它,最终应该如何编写。为此,TDD是一种耐心练习。不要写最好的代码。。。然而

  • 首先,确定代码应该做什么,
    public class MyMathObject
    {
        public MyMathObject(MyOtherClass1 firstDependency, MyOtherClass2 secondDependency)
        {                   ^---Right here                 ^---And here
    
            // implementation details
        }
    
        public int Add(int addend, int augend)
        {
            // implementation details
        }
    }
    
    private SomeInternalFunction()
    {
        var firstDependency = ServiceLocatorObject.Resolve<MyOtherClass1>();
        // implementation details
    }
    
    public class MyMathObject
    {
        public int Add(int addend, int augend)
        {
            // implementation details
        }
    }