我估计你已经能猜到这个项目有什么用途。在文章的第一版本,只是随便讨论了单元测试。行业和个人经验都证明:在生产高质量产品(这些产品更能适应做改变)测试驱动开发是一个关键的因素。而且,使用测试驱动方法往往会生产更好的设计,不好的方面就是会创建许多完美的有效的技术文件。看测试发展中一天的介绍对初学者非常有好处,例如,阅读Kent Beck's的《Test-Driven Development: By Example》。检查示例应用程序的单元测试给人们提供了:应用程序是怎样构建的,有什么可用的性能。当看了单元测试后,我们将进一步研究它们测试的代码。
单元测试的性能
单元测试必须运行得非常快。如果单元测试组运行得太久,开发者就会停止运行它们,然后我们需要它们一直在运行。事实上,如果测试运行的时间超过了0.1秒,测试可能太慢了。现在,如果你在过去运行过单元测试,你知道任何单元测试(要求访问一个现场数据库)运行的时间会更久。有NUnit时,你可以把测试放在categories里,每次运行测试的不同组时就会更加容易,同时大部分时间就会用在排除测试(此测试与数据库连接)。但是至少,这些“慢”测试应该在Continuous Integration环境里每晚运行。这里有一个已经分类的单元测试的例子。
[TestFixture] [Category("Database Tests")]
public class SomeTests { [Test] public void TestSomethingThatDependsOnDb()
{ ... } }
领域层单元测试
为了简单化,这个应用程序的领域层很简单,至少可以这样说。但是即使在最简单的领域层,在最小程度上,拥有每一个非异常途径(被领域层覆盖)。在命名空间BasicSample.Tests.Domain里有领域层测试。(一方面,CustomerTests.CanCreateCustomer测试每一个属性的getter/setter时,它是不是非常具有杀伤力?在这个过程中你抓住了几个琐碎的bug)。在阅读这篇文章时,你可能会注意到类型DomainObjectIdSetter.cs;下面将讨论建立这个类型的动机。
为了运行单元测试,打开NUnit,转到File/Open Project,打开BasicSample.Tests/bin/Debug/BasicSample.Tests.dll.为了阻止消耗时间的测试继续运行,转到NUnit的Categories
t键,双击“数据库测试”和"Web Smoke Tests."此外,点击底部的“删除这些类型”。现在,当你运行单元测试时,只有域名逻辑测试将运行,而且不会被HTTP和数据库访问测试减速。对于这样一个小的应用程序,补充的顶端的“慢”测试是可以忽略的,但是要增加时间来运行更大的应用程序的单元测试。
为数据访问层使用Test Doubles 。
在进入模拟数据库层之前,要注意这里有一个命名法来描述模拟服务的不同类型。Dummies,fakes, stubs and mocks都用来描述模拟行为的不同类型。对这些区别的总看法引起了一些东西,这些东西将包含在Gerard Meszaros' 即将出版的XUnit Test Patterns里。Meszaros提供了“双重测试”,一般用来描述任何这些行为。Stubs和mocks就是例子代码里显示的这样两个双重测试。
除非你明确测试DAO类别,通常你不需要运行单元测试,这些单元测试依靠现场数据库。本质上,它们非常慢,而且不稳定。例如:如果数据改变了,测试就崩溃了。当测试域名逻辑时,如果数据库改变,单元测试就不会崩溃。但是主要的障碍就是域名对象它们自己可能依靠DAOs。使用抽象的工厂模式(在例子中存在(以后将讨论))和连接的DAO接口,我们能把DAO双重测试注入到域名对象中,以此来模拟与数据库通信。在CustomerTests.CanGetOrdersOrderedOnDateUsingStubbedDao中包括了一个例子。下面的片段,从单元测试创建了DAO stub,并且通过公共的安装员,把它注入到customer中。因为安装员仅仅希望执行IOrderDao接口,stub DAO就会简单地代替所有现场数据库行为。
Customer customer = new Customer("Acme Anvils"); customer.ID = "ACME"; customer.OrderDao = new OrderDaoStub();
编写stub DAO 的另一个选择,可以静态地大量生成“无需实现接口”的代码,而这些可以通过如Rhino
Mocks 或NMock工具模拟DAO。无论哪个都是非常好的选择,但是Rhino Mocks 以一种强而有力的方式调用方法,而不是使用数字符,而NMock
就是使用数字符。这使得能检查它的使用编译时间,协助重命名属性和方法。演示示例CustomerTests.CanGetOrdersOrderedOnDateUsingMockedDao
告诉人们使用Rhino Mocks 3.0来创建模拟的IOrderDao 。尽管看起来建立一个模拟的对象比建立一个stub更复杂。补充的灵活性和很大程度上减少的“没有执行”代码是有力的好处。下面的代码,在类型MockOrderDaoFactory.cs中找到的,展示了IOrderDao是怎样和Rhino
Mocks一起模拟的。本质上,它建立了一个“静态”模拟,或者一个stub,事实上,在变元中经过了什么并不重要:它总是返回同一个例子命令(由TestOrdersFactory创建)。但是和Rhino
Mocks模拟并没有限制在dumb reflexes上,例如:这个可以像要求的一样有响应。
public IOrderDao CreateMockOrderDao() { MockRepository
mocks = new MockRepository(); IOrderDao mockedOrderDao
= mocks.CreateMock<IOrderDao>(); Expect.Call(mockedOrderDao.GetByExample(null)).IgnoreArguments()
.Return(new TestOrdersFactory().CreateOrders()); mocks.Replay(mockedOrderDao);
return mockedOrderDao; }
很不幸地,而且通常大都是不幸的,你正在维护一个legacy 代码,这个legacy code没有完美的"代码-接口"形式,双重测试注入允许存在完美的"代码-接口"。通常,这里在具体对象上有许多详细的依赖项,很难用test
doubles代替数据访问对象,以此来模拟现场数据库。在这些情况中,你的选择要么在测试中,refactor
legacy code来安装,要么使用一个对象模拟工具(例如:TypeMock)。有了TypeMock,就可能模拟密封的和单一的类型-没有这样一个工具却来实现一个感人的宴会。
Albeit很有力,除非非得需要,不然TypeMock就被放在旁边;过早地使用TypeMock使得不编译接口非常诱人。当操作遗留代码-时间和预算允许时,更正确的方法就是refactor
the code,取得更大的灵活性。Michael Feathers的《有效地与遗留代码工作》是包含了很多关于如何在测试工作中重构遗留代码的好办法的书。
NHibernateDAO的单元测试
在这篇文章的前一版本,Nhibernate的ISession通过System.Runtime.Remoting.Messaging.CallContext单例保存和读取。尽管非常适合WinForms 和单元测试,对ASP.NET 来说,却是一个不好的方法,因为ISession 可能在载入时会丢失(这两篇文章给予了进一步的解释:为什么在ASP.NET应用程序里使用CallContext是一个坏的方法)。为了正确地与ASP.NET 应用程序连接,应该将ISession 储存在HttpContext.Current.Items里,但是这样做,当运行单元测试时,会强迫你模拟一个HTTP文本。它同样阻止将结构简单地传递到WinForms。一个更好的方法就是在它正确的时候,使用正确的repository。因此如果一个网页文本可用,然后使用HttpContext;然而,使用CallContext. 将在后面讨论这个结合方式的执行细节。(要感谢许多对这篇文章给予评论,从而引起了这个关注)。阅读与怎样管理ISession有关的
BasicSample.Tests/Data/CustomerDaoTests.cs这篇文章,因为单元测试HTTP-agnostic。一方面,你将在单元测试看到,除非你像在你的测试中改变数据,来服从于数据库,反转处理才是一个好的方法。
正如在示例中显示的,可能创建一个通用的DAO,这个一般的DAO为任何持久稳固的对象工作。(后面将详细讨论)这就引起了应该测试什么以及怎样测试它的争论。每一个具体的DAO都要全面测试吗?怎样维护测试数据?个人经验给出了这些建议: