我一直在努力找出编写测试友好代码的最佳实践,但更具体地说是与对象构造相关的实践。在蓝皮书中,我们发现在创建对象时应该强制执行不变量,以避免我们的实体,值对象等的损坏,考虑到这一点,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
对象时我们都可以确定该对象是“有效的”。
现在让我们从测试驱动的角度来看这个例子。
我想构建测试友好的代码但是为了能够隔离测试我的Car
对象,我需要为每个依赖创建一个模拟存根或一个虚拟对象来创建我的对象,即使我可能只是想测试一个只使用其中一个依赖项的方法,比如StartEngine
方法。遵循Misko Hevery的测试理念我想编写我的测试,明确指出我不关心Door或Wheel对象只是将null引用传递给构造函数,但是因为我正在检查null,所以我不能这样做
这只是一小段代码,但是当你面对一个真正的应用程序时,编写测试变得越来越难,因为你必须解决你的主题的依赖性
Misko建议我们不要滥用代码中的空值检查(这与设计合同相矛盾)因为这样做,编写测试会变得很痛苦,作为替代方案,他说编写更多的测试比“只有”更好我们的代码是安全的,因为我们在任何地方都有空检查“您对此有何看法? 你会怎么做? 什么应该是最佳做法?
答案 0 :(得分:6)
我需要为每个依赖项创建一个模拟存根或一个虚拟对象
这是常见的说法。但我认为这是错误的。如果Car
与Engine
对象相关联,那么为什么在单元测试Engine
类时不要使用真正的 Car
对象?
但是,有人会声明,如果你这样做,你就不是 unit 测试你的代码;你的测试取决于Car
类和Engine
类:两个单元,所以是集成测试而不是单元测试。但那些人也嘲笑String
课吗?还是HashSet<String>
?当然不是。单元测试和集成测试之间的界限不太明确。
更哲学上,在许多情况下,无法创建好的模拟对象。原因在于,对于大多数方法,对象委托给关联对象的方式是 undefined 。是否委托以及如何将合同留作实施细节。唯一的要求是,在委托时,该方法满足其委托的前提条件。在这种情况下,只有功能齐全的(非模拟)委托才会这样做。如果真实对象检查其前提条件,则无法满足委托的前提条件将导致测试失败。调试测试失败很容易。
答案 1 :(得分:4)
查看test data builders的概念。
使用预配置数据创建构建器一次,必要时覆盖属性并调用Build()
以获取正在测试的系统的新实例。
或者您可以查看Enterprise Library的来源。测试包含一个名为ArrangeActAssert
的基类,它为BDD-ish测试提供了很好的支持。您可以在从AAA派生的类的Arrange
方法中实现测试设置,并且只要您运行特定测试,就会调用它。
答案 2 :(得分: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
}
答案 3 :(得分:1)
您应该考虑使用工具为您完成此类工作。像AutoFixture一样。从本质上讲,它创建了对象。听起来很简单,AutoFixture可以完全按照您的需要进行操作 - instantiate object使用一些您不关心的参数:
MyClass sut = fixture.CreateAnnonymous<MyClass>();
MyClass
将使用构造函数参数,属性等的虚拟值创建(请注意,这些不是默认值,如null
,而是实际实例 - 但它归结为相同事物;需要存在的伪造的,无关的价值观。)
编辑:稍微扩展一下介绍......
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>();
这就是全部。将使用Moq生成的模拟创建testedClass
。请注意,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一个外观 - 这是一个很棒的库。
答案 4 :(得分:0)
我知道不是每个人都同意我(我知道Mark Seemann会不同意我的观点),但我通常不会在构造函数中对使用构造函数注入的容器创建的类型执行null检查。出于两个原因,首先它(有时)使测试变得复杂 - 你已经注意到了 - 。但除此之外,它只会给代码增加更多噪音。所有DI容器(我知道)都不允许将空引用注入到构造函数中,因此我无需将代码复杂化,无论如何都不会发生。
当然你可以争辩说因为我为我的服务类型留下了空检查,这些类型现在隐含地知道DI容器的存在,但这是我可以为我的应用程序使用的东西。在设计可重用框架时,事情当然是不同的。在这种情况下,您可能需要进行所有空检查。