雖然這部份內容並無過多地討論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設計原則的支持。