首页 文章

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

提问于
浏览
5

我一直试图找出编写测试友好代码的最佳实践,但更具体地说是与对象构造相关的实践 . 在蓝皮书中,我们发现我们应该在创建对象时强制执行不变量,以避免我们的实体,值对象等的损坏,考虑到这一点,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();
   }
}

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

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

我想构建测试友好的代码但是为了能够隔离地测试我的 Car 对象我需要为每个依赖创建一个模拟存根或虚拟对象来创建我的对象,即使我可能只是想要测试一个只使用其中一个依赖项的方法,如 StartEngine 方法 . 遵循Misko Hevery的测试理念我做了'd like to write my test specifying explicitly that I do not care about the Door or Wheel objects just passing null reference to the constructor, but since I am checking for nulls, I just can' t

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

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

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

5 回答

  • 1

    看看test data builders的概念 .

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

    或者你可以看一下Enterprise Library的来源 . 测试包含一个名为ArrangeActAssert的基类,它为BDD-ish测试提供了很好的支持 . 您可以在从AAA派生的类的 Arrange 方法中实现测试设置,并且只要您运行特定测试,就会调用它 .

  • 0

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

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

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

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

  • 1

    我在单元测试中解决了这个问题:

    我的汽车测试类看起来像:

    public sealed class CarTest
    {
       public Door Door { get; set; }
       public Engine Engine { get; set; }
       public Wheel Wheel { get; set; }
    
       //...
    
       [SetUp]
       public void Setup()
       {
          this.Door = MockRepository.GenerateStub<Door>();
          //...
       }
    
       private Car Create()
       {
          return new Car(this.Door, this.Engine, this.Wheel);
       }
    }
    

    现在,在测试方法中,我只需要指定“有趣”的对象:

    public void SomeTestUsingDoors()
    {
       this.Door = MockRepository.GenerateMock<Door>();
       //... - setup door
    
       var car = this.Create();
       //... - do testing
    }
    
  • 6

    你应该考虑为你做这种工作的工具 . 喜欢AutoFixture . 从本质上讲,它创建了对象 . 听起来很简单,AutoFixture可以完全满足您的需求 - instantiate object带有一些您不关心的参数:

    MyClass sut = fixture.CreateAnnonymous<MyClass>();
    

    MyClass 将使用构造函数参数,属性等的虚拟值创建(请注意,那些不是默认值,如 null ,但实际实例 - 但它归结为相同的东西;伪造的,无关的值需要在那里) .

    Edit: 稍微扩展一下介绍......

    AutoFixure还附带AutoMoq扩展功能,以便成为全面的auto-mocking container . 当AutoFixture无法创建对象(即接口或抽象类)时,它会将创建委托给Moq - 这将创建模拟 .

    所以,如果你有类这样的构造函数签名类:

    public ComplexType(IDependency d, ICollaborator c, IProvider p)
    

    在不关心任何依赖关系而只想要 nulls 的场景中进行测试设置将完全由2行代码组成:

    var fixture = new Fixture().Customize(new AutoMoqCustomization());
    var testedClass = fixture.CreateAnonymous<ComplexType>();
    

    这就是全部 . testedClass 将使用Moq在引擎盖下生成的模拟创建 . 请注意 testedClass is not a mock - 这是你可以测试的真实对象如果你用构造函数创建它 .

    它变得更好 . 如果你想通过AutoFixture-Moq动态创建一些模拟但是你想要更多控制的其他一些模拟,例如 . 在给定的测试中验证?您只需要一行代码:

    var fixture = new Fixture().Customize(new AutoMoqCustomization());
    var collaboratorMock = fixture.Freeze<Mock<ICollaborator>>();
    var testedClass = fixture.CreateAnonymous<ComplexType>();
    

    ICollaborator 将是你完全访问的模拟,可以做 .Setup.Verify 和所有相关的东西 . 我真的建议给AutoFixture一个看 - 它是很棒的库 .

  • 4

    我知道不是每个人都同意我(我知道Mark Seemann会不同意我),但我通常不会在构造函数中对使用构造函数注入的容器创建的类型执行null检查 . 有两个原因,首先它(有时)使测试变得复杂 - 你已经注意到了 - . 但除此之外,它只会给代码增加更多噪音 . 所有DI容器(我所知道的)都不允许将空引用注入到构造函数中,因此我无需将代码复杂化为无论如何都不会发生的事情 .

    当然你可以争辩说,因为我为我的服务类型留下了空检查,这些类型现在隐含地知道DI容器的存在,但这是我可以为我的应用程序使用的东西 . 在设计可重用框架时,事情当然是不同的 . 在这种情况下,您可能需要进行所有空检查 .

相关问题