虽然这部份内容并无过多地讨论Apworks框架的使用,但这部份内容很是重要,它与Apworks框架自己的设计紧密相关,也是进一步了解Apworks框架设计的必修课。程序员
在敏捷开发过程当中,单元测试是很是重要的。这不一样于传统的瀑布开发模型,在瀑布模型中,单元测试的重要性体现的并不明显,由于在这种模型中,“测试”被强调为整个开发流程中的一个环节,也会有专门的测试团队来负责测试过程,因而,由开发人员负责的单元测试每每被忽略。另外一方面,在项目刚刚开始时,因为团队对开发过程和规范的重视,开发人员会着手一部分单元测试的编写工做,但随着时间的推移、需求的变化以及项目进度的推动,出于项目上线或者产品发布的压力,开发团队逐渐更多地关注功能的实现和缺陷(Bug)修复,单元测试也就随手扔在一边。最终,咱们可以看到的结果就是,绝大多数项目中都包含了单元测试的工程,但这些单元测试的工程又每每都是被遗弃已久,甚至是没法编译的。算法
然而,在实践敏捷开发的项目中,单元测试是很是重要的,这在文章的第一部分就作了简单的介绍。这种重要性源自于敏捷开发过程的基本特性:以持续集成的方式应对不断变化的客户需求。软件系统开发的一个重要特色就是需求的不肯定性和可变性,传统的瀑布模型以循序渐进的方式开发软件系统,很明显没法有效地应对这种可变性特色。敏捷开发过程以迭代的方式进行,项目负责人(Product Owner)将整体需求分割成多个用户故事(User Story),并根据重要优先级,在产品功能特性列表(Product Backlog)中对这些用户故事进行排序。另外一方面,整个开发过程被分为多个迭代(Sprint),项目负责人会根据开发团队的预估点数(Estimated Points),结合上一个迭代团队的完成能力(Velocity),从产品功能特性列表中挑选一些用户故事来实现。在开发过程当中,项目负责人以及客户都会参与进来,不只如此,在每次迭代结束的时候,团队都会向项目负责人进行功能演示,假若功能实现上有所出入,那么这些功能上的修改将被记录在案,以便在后续的迭代中改进,这样就可以保证所开发的软件系统不会偏离实际需求太远,为项目或产品的最终成功交付奠基了基础。数据库
固然本文的主要宗旨并非对敏捷开发过程进行论述,而是更多地考虑,当出现需求变动、功能改进,以及增添新功能的状况时,咱们应该怎么办。遇到这样的问题,咱们大体应该从两方面考虑:一、以哪一种方式修改设计和代码最合适?最好是既简单快捷,又能尽可能避免形成已有设计和代码的大范围修改;二、一旦发生设计和代码的修改,如何保证(或者说得知)这些修改不会影响到已经实现并通过严格测试的系统功能?不幸的是,要可以作好其中的任何一方面,都不是件容易的事情。前者要求整个软件系统有着良好的设计,然后者则要求这种设计是可测试的。编程
首先,团队应该更合理地将面向对象的分析和设计技术引入到软件系统的开发中来,对系统分析和设计引发足够的重视。或许有人会说当今流行的软件开发方法论有不少,好比面向函数式编程,也时常看到有一部分人会对面向过程的“面条式”编程情有独钟。固然,对于像我这种天天都浸泡在面向对象世界里的程序员而言,使用一些面向过程的方式编写一些小程序也别有一番趣味,但不得不认可的是,当今大型企业级复杂软件系统开发中,面向对象分析和设计技术仍然占据着权威性的主导地位,纵观流行的开发技术和平台:.NET、JAVA、C++都是以面向对象为基础的,Python、PHP、Ruby、Lua等等,对面向对象技术也有着很好的支持。事实上,面向对象技术已经为咱们的第一个问题提供了答案,咱们须要作的是,在项目中合理地利用这种技术,而这偏偏也就是最大的难点,它要求团队有着较高的技术素养和丰富的实战经验。小程序
我曾经作过这样一个项目,在这个项目的一个迭代中,团队须要实现这样的功能:在一些特定的条件下,好比当用户在线注册3天后,或者每一个季度结束的时候,可以在用户的我的信息主页上看到报表的显示连接,当用户点击这些连接时,可以打开并查看相应的报表。这样的需求实现起来并不困难,最直接的方式就是在打开页面的时候从后台对这些条件进行判断,以决定是否显示相应的连接。然而,在通过细致的分析以后,咱们发现,使用基于面向对象的事件模型来解决这样的问题会显得更加天然,而且易于扩展:几乎全部的断定条件都是以“当……时,将会(将可以)……”的句式进行描述,这就是事件模型的经典应用场景。以后所发生的事情让咱们庆幸当时的选择是正确的:在下一个迭代中,客户要求不只要可以在用户的我的信息主页上看到报表的显示连接,并且还要以电子邮件的形式通知客户:咱们已经为您准备了一份报表,请登陆您的我的主页进行查看。接下来,咱们向已有的系统添加了一个新的模块,用来侦听来自事件模型的消息,而且在消息处理器(Event Handler)中,根据消息数据和电子邮件模板来产生一封邮件,并将其发送出去。咱们获得的结果是:彻底没有改变已有代码的任何部分,所以已有代码不须要进行回归测试,咱们仅仅是添加了一个模块,修改了程序的配置文件,并对这个模块作了单元测试和集成测试,整个过程仅仅花了团队不到一周的时间,而每一个迭代倒是覆盖了三周的时间。这不只提升了团队的生产率,并且保证了项目和产品的质量。在这个案例中,咱们没有选择那种直观而且易于实现的方式,而是对功能需求进行了细致的设计,并选择了一种相对较为复杂的方式,然然后续的故事验证了这种取舍的正确性。假若咱们选择了直观简易的方式,那么当客户需求更改或者功能须要添加的时候,咱们须要对已有代码中全部产生报表连接的部分进行修改,添加电子邮件的发送功能,咱们须要改变已有的测试用例,以知足新的需求,咱们还须要对这些修改过的代码进行回归测试,以确保以前的报表连接可以正确产生,三周时间或许勉强可以完成这些工做。别忘了一件更让人头疼的事情:在下一个迭代中,客户要求咱们不只须要发送电子邮件,还须要根据用户本身的隐私设置,选择性地向他们发送短消息提醒。好吧!代码再改一次,测试用例再改一次,再作一次回归测试。其实,面向对象分析和设计的基本原则早就提醒过咱们,这种作法会引来无穷的隐患:咱们的作法从根本上违反了“开-闭原则(Open-Closed Principle)”!框架
以上是一个真实的案例,我相信重视并合理利用面向对象分析和设计技术的好处,不用我再用过多的笔墨去论证,我想阐述的是,开发前的分析和设计的确须要花费必定的时间,但团队不要在这方面过于吝啬,分析和设计作好了,便可以在后续开发过程当中受益(好比节省时间、提升质量),并且多数状况下,团队的受益每每要多于以前在分析和设计上的付出。对于这一点,有些读者或许会有不一样的观点,这也很正常,毕竟项目的实际状况会有所不一。好比嵌入式硬件驱动的开发,或者是化合物分子量计算算法的实现等等,在这些场景中,或许采用结构化编程的方式效率更高更快捷,因而也就不存在上面讨论的这些问题了。函数式编程
既然咱们对项目代码进行了改变,添加了新的模块也好,经过重构改善既有设计也好,咱们老是须要保证这些改变不会影响到已有的功能实现,这也就是上面所提到的第二个问题。从开发人员的角度看,解决这个问题最好的办法就是每完成一次代码更改,都将全部的单元测试所有运行一次,确保全部的单元测试都能顺利经过,若是单元测试的代码覆盖率比较高的话,那么单元测试的所有经过就表示测试所覆盖的代码行为跟先前的行为是一致的,也就是说,新的更改并无影响到已有的代码功能。从整个项目的角度出发,这实际上是一种持续集成的软件开发实践:开发人员会常常集成这些代码变动,一般每一个成员天天至少集成一次,也就是项目上天天会发生屡次集成,每次集成都经过自动化过程(编译、部署、自动化测试)来验证,从而在尽量早的阶段发现错误,减少因设计和代码的变动带来的质量风险。函数
因而可知,单元测试对敏捷项目是多么的重要。因此,单元测试不只要写,并且也要进行合理的设计,以提升单元测试的代码覆盖率。一般来说,单元测试的设计和编写应该遵循如下几个原则:单元测试
因而,咱们编写的代码就应该可以让针对这些代码的单元测试知足以上的原则,这也就是咱们平时提的最多的“代码可测试性”。如何让代码可测试?采用基于抽象的面向对象设计技术,能够帮助咱们知足这样的需求。测试
说明:在Stackoverflow上有过这样的讨论:由单例模式(Singleton)实现的代码可测试吗?在众多答案中,更为合理的解释是:虽然经过Fake技术能够实现代码的可测试性,但单例模式不是最好的设计。单例无非是更改了对象的生命周期,可以达到相同效果的一个更合理的设计是基于抽象(接口)进行设计,而后使用依赖注入框架来管理对象的生命周期。这种作法不只灵活度高,并且设计自己是可测试的。
举一个很简单的例子:在ASP.NET MVC/Web API的控制器(Controller)中,咱们会使用仓储来读取聚合根,而后执行相关的业务操做。好比不少状况下,咱们会这么作:
public class MyController : ApiController { private readonly CustomerRepository customerRepository = new CustomerRepository(); public IHttpActionResult GetCustomerById(Guid id) { var customer = customerRepository.GetByKey(id); // ... return Ok(); } }
这种设计大体能够用下面的UML类图表示:
上面的代码产生了MyController与CustomerRepository之间的关联(Association)关系。这种关系致使MyController的实现依赖于CustomerRepository。假设咱们须要对GetCustomerById进行单元测试,咱们势必须要构造一个MyController的实例,而此时CustomerRepository也被构造,因而对MyController的单元测试须要依赖于CustomerRepository的实现。从实现上看,咱们首先须要配置一个Customer仓储,使其可以正常工做,而后将测试数据导入到仓储中,进而再对GetCustomerById进行所谓的单元测试。在测试运行时,测试用例会主动访问数据库或者其它的数据存储机制,来得到特定的数值,而后断定咱们须要的结果是否正确。
相信不少项目会这么去作单元测试,固然不排除有些项目自己存在历史遗留问题的可能性,其实这种作法更多地包含了集成测试的元素:它整合了外部资源的访问。就单元测试而言,首先测试的执行是缓慢的,外部资源的访问大大下降了测试效率;其次测试是不稳定的,若是外部资源访问失败,或者测试数据发生了更改,咱们的测试用例就会失败,而这却不是咱们所须要的;最后,若是CustomerRepository的实现发生了变化,咱们不只须要对整个控制器进行从新编译,并且全部的单元测试都须要从新运行一次,紧耦合给咱们带来了无限困扰。对上述代码的重构已经刻不容缓。
咱们能够引入一个ICustomerRepository的接口,并使得MyController仅关联ICustomerRepository接口,而CustomerRepository则实现了这个接口。若是你仍是在MyController中直接使用new关键字来新建CustomerRepository的实例,好比:
private readonly ICustomerRepository customerRepository = new CustomerRepository();
那么这种作法与上面的作法仍是没有区别:MyController仍然依赖于ICustomerRepository接口的一种实现。正确的作法应该是经过MyController的构造函数,将ICustomerRepository的实现类型传入,这样就彻底解耦了MyController和CustomerRepository。参考代码以下:
public class MyController : ApiController { private readonly ICustomerRepository customerRepository; public MyController(ICustomerRepository customerRepository) { this.customerRepository = customerRepository; } public IHttpActionResult GetCustomerById(Guid id) { var customer = customerRepository.GetByKey(id); // ... return Ok(); } } public class CustomerRepositoryImpl : ICustomerRepository { }
如下是UML类图:
若是咱们对这样的设计进行单元测试,咱们可使用Mock技术,建立ICustomerRepository的桩(Stub)对象,同时假设在调用这个桩对象的GetByKey方法时,返回某个特定的Customer实例,从而验证MyController中GetCustomerById方法的逻辑正确性。难怪社区中会有人认为,单元测试实际上是一个验证的过程。基于这种设计,对于GetCustomerById方法的单元测试能够这样写(使用Moq Framework):
[TestMethod] public void GetCustomerByIdTest() { var customer = new Customer(); Mock<ICustomerRepository> mockCustomerRepository = new Mock<ICustomerRepository>(); mockCustomerRepository .Setup(x => x.GetByKey(It.IsAny<Guid>())) .Returns(customer); var myController = new MyController(mockCustomerRepository); var returnedCustomer = myController.GetCustomerById(Guid.NewGuid()); Assert.AreEqual(customer, returnedCustomer); }
回顾上面的单元测试设计原则,显而易见这样的单元测试是知足要求的。经过这个简单的案例咱们也能够看到,合理的系统设计对于单元测试的编写是何等的重要。虽然Microsoft Visual Studio 2012/2013 Fake Framework(在Visual Studio 2010中须要额外安装Microsoft Pex and Moles扩展)还有TypeMock等收费的Mock框架经过必定的技术可以作到对于不可测试的代码进行单元测试,但这不是完美的解决方案,这些Mock框架仍是有必定的局限性。从系统开发的角度出发,咱们更但愿可以让咱们设计和开发的软件是稳定的、高效的、可测的、灵活的,以及可维护的。总而言之,咱们的设计应该是可测的,这一点对于敏捷开发实践尤为重要,或者直接从单元测试入手,以测试驱动开发(Test-Driven Development)的方式,一步步地实现一个可测的设计。
在此咱们再也不细究单元测试中Stub、Mock以及Fake的概念,咱们须要反复强调的是合理设计的重要性。对于面向对象分析与设计(OOAD)而言,遵循SOLID设计原则是很是重要的,系统设计的好坏将直接影响到项目管理(需求管理、资源分配、成本管理、进度管理等方面),甚至整个项目的成败。
依赖倒置原则是OOAD “SOLID”设计原则中“D”所表示的意思。这是一个颇有趣的事情,让咱们以通俗的方式来理解这个问题。仍然以咱们上面改进后的设计为例,假若如今咱们要在Visual Studio中开发这么一个Web API,你会将这些类和接口写在哪一个或者哪些程序集(Assembly)中?你会将全部的类和接口都定义在Web API这个项目中吗?
若是你的答案是:No,那么进一步考虑这个问题:你会将ICustomerRepository接口和它的实现类:CustomerRepository类定义在同一个项目(也就是Class Library项目)中吗?以下:
此时你的答案或许会是:Yes。咱们再进一步思考,若是是这样的话,WebAPI项目就要依赖于Repositories项目,表面上看没什么不妥,而实际上每当CustomerRepository的实现发生更改,咱们都要从新编译和发布整个WebAPI,而CustomerRepository的变动却又是WebAPI所不关心的,由于它根本无需关心ICustomerRepository接口是如何实现的。另外一方面,若是咱们新增长了一种ICustomerRepository的实现,那么这个新的实现也要引用MyProject.Repositories项目,因而,CustomerRepository的变动又会影响到这个新实现所在的程序集。
通过分析咱们不难发现,合理的作法应该是将ICustomerRepository接口定义在WebAPI项目中,也就是:
缘由很简单:WebAPI项目中的MyController仅依赖于ICustomerRepository接口,而不是CustomerRepository这个具体的实现类型。这也不难理解,接下来的事情就比较有趣了:在咱们编写CustomerRepository代码的时候,咱们要在Visual Studio中,在MyProject.Repositories项目上添加对MyProject.WebAPI项目的引用,不然没法得到ICustomerRepository这个接口的定义!简单地说,原本应该是A须要依赖B中的东西,来实现A本身的功能,如今反过来B须要引用A来实现A中抽象的部分。这就是依赖倒置的基本概念。
难道这么作不会产生循环引用吗?若是你仍是试图在MyProject.WebAPI中引用MyProject.Repositories,以获取CustomerRepository实现类,那么你的设计仍旧是糟糕的。MyProject.WebAPI为何要去关心ICustomerRepository接口的具体实现是什么样子的呢?彻底不必关心。那怎么办?使用依赖注入框架!(也就是“控制翻转/依赖注入(IoC/DI)”的由来)
其实,更为合理的设计应该是这样的:
MyProject.WebAPI和MyProject.Repositories都引用MyProject.RepositoryContracts项目,而在这个项目中,包含了全部Repository的接口定义。有兴趣的朋友能够思考一下,这种设计的优势在哪里。
本文详细讨论了优秀的设计(尤为是面向对象分析与设计)对单元测试的重要性、单元测试对持续集成的重要性,以及持续集成对敏捷开发的重要性。要实践敏捷开发,一个优雅、合理的设计必不可少。文章最后还简单讨论了依赖倒置原则,这也是Apworks框架设计所遵循的基本原则。下一部分将介绍Apworks框架设计对OOAD设计原则的支持。