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

Bel*_*lin 12 tdd unit-testing

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

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

为这个问题提供了一个例子:如果
在编写源代码时它们是对象依赖项,那么如何编写测试代码?

Dav*_*vid 23

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

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

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

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

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

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

public int Add(int addend, int augend)
{
    return ((addend * 2) + (augend * 2)) / 2;
}
Run Code Online (Sandbox Code Playgroud)

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

public int Add(int addend, int augend)
{
    return addend + augend;
}
Run Code Online (Sandbox Code Playgroud)

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

public int Add(int addend, int augend)
{
    return 2;
}
Run Code Online (Sandbox Code Playgroud)

你猜怎么着?测试仍然通过.这是我们唯一的测试,它是唯一给出的规范,因此代码"有效".显然,我们需要改进测试以涵盖更多案例.编写更多测试将为我们提供编写更多代码所需的规范.

事实上,根据TDD的第三条规则,最后一次实施应该是第一次实施:

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

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

这被称为红色,绿色,重构循环.这是一个简单的,人为设计的例子,保龄球游戏.该练习的目的是练习这个循环:

  1. 首先,编写一个期望某些行为的测试.这是循环的红色部分,因为测试将在没有行为的情况下失败.
  2. 接下来,编写代码来展示该行为.这是循环的绿色部分,因为它的目的是让测试通过.并且只是让测试通过.
  3. 最后,重构代码并改进它.当然,这是循环的重构部分.

你陷入困境的地方是你永远处于周期的重构部分.您已经在考虑如何使代码更好.什么算法是正确的,如何优化它,最终应该如何编写.为此,TDD是一种耐心的练习.不要写最好的代码...... 然而.

  1. 首先,确定代码该做什么,仅此而已.
  2. 接下来,编写执行它的代码,仅此而已.
  3. 最后,改进代码并使其更好.

UPDATE

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

大约只要我记得,我已经使用过像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
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,正如您所指出的,测试需要提供其依赖关系或模拟.在类的足迹,他们没有看到的迹象实际使用MyOtherClass1还是MyOtherClass2,反而有指示需要他们.作为依赖关系,它们由构造函数大声宣传.

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

否则,你先写下这个:

public class MyMathObject
{
    public int Add(int addend, int augend)
    {
        // implementation details
    }
}
Run Code Online (Sandbox Code Playgroud)

然后你会为它编写测试,然后你实现它并发现依赖项,然后你就会为它重新编写测试.这就是问题所在.

但是,您发现的问题不是测试或测试驱动开发的问题.问题实际上是在对象的设计中.尽管// 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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

但你最终得到的是这样的事情MyMathObject:

private SomeInternalFunction()
{
    var firstDependency = ServiceLocatorObject.Resolve<MyOtherClass1>();
    // implementation details
}
Run Code Online (Sandbox Code Playgroud)

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

public class MyMathObject
{
    public int Add(int addend, int augend)
    {
        // implementation details
    }
}
Run Code Online (Sandbox Code Playgroud)

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


Spa*_*son 2

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

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

除非你是某种天才——这第一次削减将需要引入重构和抽象、模式、可维护性、性能和所有其他因素。

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

在现实中,以这种方式工作并不总是符合要求——特别是在很难获得规范的情况下。如果您没有得到作为开发人员所需的东西,您需要小心,不要盲目地走上这条路。当继承代码或添加到“棕地”项目时,通常也是不可能实现的。作为开发人员,尽早确定实用性非常重要。