设计合同,编写测试友好的代码,对象构建和依赖注入将所有最佳实践结合在一起

Jup*_*aol 5 unit-testing design-by-contract dependency-injection constructor-injection

我一直试图找出编写测试友好代码的最佳实践,但更具体地说是与对象构造相关的实践.在蓝皮书中,我们发现在创建对象时应该强制执行不变量,以避免我们的实体,值对象等的损坏,考虑到这一点,Design By Contract似乎是避免我们的对象损坏的解决方案,但是当我们遵循这个,我们最终可能会编写如下代码:

class Car
{
   //Constructor
   public Car(Door door, Engine engine, Wheel wheel)
   {
      Contract.Requires(door).IsNotNull("Door is required");
      Contract.Requires(engine).IsNotNull("Engine is required");
      Contract.Requires(wheel).IsNotNull("Wheel is required");
      ....
   }
   ...
   public void StartEngine()
   {
      this.engine.Start();
   }
}
Run Code Online (Sandbox Code Playgroud)

嗯,这看起来很好看吗?看来我们正在构建一个安全类,暴露了所需的契约,因此每次Car创建对象时我们都可以确定该对象是"有效的".

现在让我们从测试驱动的角度来看这个例子.

我想构建测试友好的代码,但为了能够隔离测试我的Car对象我需要为每个依赖创建一个模拟存根或一个虚拟对象来创建我的对象,即使我可能只是想测试一种方法,只使用这些依赖项之一,如StartEngine方法.遵循Misko Hevery的测试理念我想编写我的测试,明确指出我不关心Door或Wheel对象只是将null引用传递给构造函数,但是因为我正在检查null,所以我不能这样做

这只是一小段代码,但是当您面对真正的应用程序时,编写测试变得越来越难,因为您必须解决主题的依赖关系

Misko建议我们不要滥用代码中的空值检查(这与设计合同相矛盾)因为这样做,编写测试会变得很痛苦,作为替代方案,他认为编写更多的测试比"只有这样的测试"更好.我们的代码是安全的,因为我们到处都有空检查"

你对此有何看法?你会怎么做?什么应该是最好的做法?

Rae*_*ald 6

我需要为每个依赖项创建一个模拟存根或一个虚拟对象

这是常见的说法.但我认为这是错误的.如果a CarEngine对象关联,为什么不在单元测试你的类时使用真实 Engine对象Car

但是,有人会声明,如果你这样做,你就不会对你的代码进行单元测试; 你的测试取决于Car类和Engine类:两个单元,所以是集成测试而不是单元测试.但那些人也嘲笑这个String班吗?还是HashSet<String>?当然不是.单元测试和集成测试之间的界限并不是那么清楚.

更哲学上,在许多情况下,您无法创建好的模拟对象.原因在于,对于大多数方法,对象委托给关联对象的方式是未定义的.是否委托以及如何将合同留作实施细节.唯一的要求是,在委托时,该方法满足其委托的前提条件.在这种情况下,只有一个功能齐全(非模拟)的代表才会这样做.如果真实对象检查其前提条件,则无法满足委托的前提条件将导致测试失败.调试测试失败很容易.


Seb*_*ber 4

看一下测试数据构建器的概念。

您可以使用预配置的数据创建构建器一次,必要时覆盖属性并调用Build()以获取正在测试的系统的新实例。

或者您可以查看企业库的来源。这些测试包含一个名为 的基类ArrangeActAssert,它为 BDD 测试提供了很好的支持。Arrange您可以在从 AAA 派生的类的方法中实现测试设置,并且每当您运行特定测试时都会调用该方法。