5

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

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创建对象时,我们都可以确定该对象是“有效的”。

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

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

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

Misko 建议我们不应该在代码中滥用空值检查(这与契约式设计相矛盾),因为这样做,编写测试变得很痛苦,作为替代方案,他说最好编写更多的测试,而不是“有这样的错觉”我们的代码是安全的,因为我们到处都有空检查”

您对此有何看法?你会怎么做?最佳实践应该是什么?

4

5 回答 5

6

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

这是常说的。但我认为这是错误的。如果 aCar与对象相关联,为什么在对您的类进行单元测试时Engine不使用真实 对象?EngineCar

但是,有人会声明,如果您这样做,您不是在对代码进行单元测试;您的测试取决于Car类和Engine类:两个单元,因此是集成测试而不是单元测试。但是那些人也嘲笑String课堂吗?或者HashSet<String>?当然不是。单元测试和集成测试之间的界限不是很清楚。

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

于 2012-03-14T13:28:53.840 回答
4

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

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

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

于 2012-03-14T10:56:29.810 回答
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
}
于 2012-03-14T10:28:42.243 回答
1

您应该考虑使用工具来为您完成此类工作。喜欢AutoFixture。本质上,它创建对象。听起来很简单,AutoFixture 可以完全满足您的需要 -使用一些您不关心的参数实例化对象

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

MyClass将使用构造函数参数、属性等的虚拟值创建(请注意,这些不会是默认值null,而是实际实例 - 但归结为同一件事;需要存在的伪造的、不相关的值)。

编辑:为了扩展介绍一点......

AutoFixure 还附带 AutoMoq 扩展,成为成熟的自动模拟容器。当 AutoFixture 无法创建对象(即接口或抽象类)时,它将创建委托给 Moq - 它将创建模拟。

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

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

当您不关心任何依赖项并且只想要nulls时,您的测试设置将完全由两行代码组成:

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

这就是全部。testedClass将使用 Moq 在后台生成的模拟来创建。请注意,这testedClass 不是模拟- 它是您可以测试的真实对象,就像您使用构造函数创建它一样。

它变得更好。如果您希望 AutoFixture-Moq 动态创建一些模拟,但您希望拥有更多控制权的其他模拟,例如。在给定的测试中进行验证?您只需要一行额外的代码

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

ICollaborator将是您可以完全访问、可以做以及所有相关内容的.Setup模拟.Verify。我真的建议给 AutoFixture 看看 - 这是一个很棒的库。

于 2012-03-14T11:28:16.570 回答
0

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

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

于 2012-03-14T11:42:57.757 回答