加入RUN!PC粉絲團
最近新增的精選文章
 
最多人點閱的精選文章
 
 
精選文章 - 開發技術
分享到Plurk
分享到FaceBook
 
利用VS2010平台做單元測試(3)--
進階單元測試實務(上)
文‧圖/陸雲中 2011/7/13 下午 06:08:05

原刊載於RUN!PC雜誌的[技術專欄],筆者已介紹如何使用Visual Studio 2010內建的功能進行單元測試。本文將區分為上、下兩篇,進一步介紹進階的概念,同樣的,會使用到已刊登在RUN!PC雜誌的文章,所運用的範例程式碼專案。

首先,先說明一下單元測試必須符合的條件。在筆者的經驗中,單元測試經常會與介面整合測試混為一談,這是因為大部分開發人員對於單元測試的定義並不清楚,一般而言,好的單元測試必須符合以下幾項條件:

高隔離性(Isolation)
單元測試應該只測試某個類別方法內的程式碼,其他與此類別方法無關的程式碼都應該被隔離。如何「隔離」與類別方法無關的程式碼呢?通常是使用一種稱為Mock的技術,文章後會進一步說明。

執行速度快
單元測試要能快速執行完,可試想一個狀況:若測試專案內有1000個單元測試,每個單元測試若需要執行1秒鐘。那麼,完整執行完所有單元測試需要1000秒,也就是將近17分鐘!而一個測試專案有上千個單元測試是非常可能發生的,在這種情形下,單元測試的速度就相當重要了。

高獨立性(Self Containment)
單元測試所需要的資料必須含在單元測試中,不需要依賴外部提供,例如,資料驅動式的單元測試需要的資料由檔案提供(如Excel)而不是存放在資料庫中就是一種高獨立性。高獨立性可以減少執行單元測試所需的環境設定問題與加快執行速度,而且,資料以檔案型式存放可以同時與單元測試程式碼一起版本控管,維護上更為方便。

多工(Race Safe)
單元測試必須要能夠在同一時間由多位開發人員同時執行,並且要能夠得到一致的執行結果。

低相依性(Independence)
單元測試之間不能夠有相依性,也就是說,單元測試之間的執行次序不能夠影響單元測試執行結果。換句話說,每一個單元測試都必須能夠以任何順序執行,並且執行後能夠得到一致的測試結果。

可維護性(Maintainable)
單元測試必須要視為應用系統程式的一部分維護,如此才能確保單元測試程式碼與應用系統程式的一致性。為了能確保單元測試的可維護性,通常都會定期(例如:夜間)由自動化系統(例如:TFS的Team組建)自動執行所有的單元測試。而所有的開發人員也應該在簽入程式碼前執行所有相關的單元測試,確保簽入的程式碼沒有影響之前的單元測試。

使用Visual Studio 2010內建的單元測試功能,並無法滿足上述高隔離性的要件。因為Visual Studio 2010內建的Unit Test Framework 並沒有提供Mock功能。接下來對Mock進一步說明。

Mock簡介

Mock是一種「模擬」特定類別物件(object)的技術。當一個類別物件被Mock後,該物件的所有行為,例如方法、屬性、建構式等等都可以被重新定義,所謂的重新定義就是Mock物件的輸出輸入是可以控制的。這種技術對於簡化單元測試開發相當重要。

有了Mock技術,單元測試可以把與目前單元測試無關的類別隔離,並且控制其輸出輸入,如此能夠加快單元測試的執行速度與簡化單元測試執行時的相關環境設定問題,並且可以使單元測試更準確地測試需要測試的程式碼,提升單元測試的品質。

一般來說,單元測試與測試類別方法的關係如圖1所示。其中表示單元測試直接呼叫測試類別方法,取得回傳值後驗證是否符合驗證結果。但是,實務上通常都比圖1的情況更複雜許多。一般應用程式系統的類別方法間都有高度相依性,也就是說,不同類別方法會互相呼叫,而且通常不只一層。


▲ 圖1:基本單元測試

舉例而言,類別方法A會呼叫另一個類別方法B,類別方法B中又會呼叫另一個類別方法C。實務上的單元測試例子如圖2所示,說明了一個測試類別方法可能會呼叫資料庫類別來處理資料庫相關的工作,也可能會呼叫Log類別處理Log記錄工作。另外,也可以利用Web Service類別呼叫遠端伺服器處理相關工作。資料庫類別會直接與資料庫溝通,Log類別與Web Service類別則各自直接與外部系統溝通。


▲ 圖2:實務單元測試範例。

在這個例子中,會發現一個問題:如何撰寫這種類別方法的單元測試呢?最簡單的方式是用圖1的方式直接呼叫,這樣一來,撰寫單元測試時必須要準備好完整的測試環境,如資料庫、Log服務、遠端伺服器等等。只要其中一項有問題,單元測試就會失敗,但是這種測試失敗屬於外部系統問題,不屬於程式碼問題。如此會花費不必要的時間釐清排除問題。

更進一步的說,即使所有外部系統都準備妥當,如果相關類別(如資料庫類別)程式有錯誤,依然會造成這個單元測試失敗!但是,這同樣不屬於這個類別方法的範圍,且很有可能這是由其他開發人員所撰寫的,卻要由撰寫這個單元測試的開發人員排除,這樣都會造成撰寫單元測試的複雜度與成本提高,而且,最糟糕的是,單元測試已經變成功能整合測試了!負責上層類別的開發人員變成要負責所有呼叫到的下層類別是否正常,在這種狀況下,開發人員可能要花費比撰寫應用系統程式碼更多的心力在撰寫單元測試上,造成生產力下降。那麼,要如何改善這個狀況呢?答案是使用Mock來進行類別隔離(Isolation)的動作,如圖3所示。


▲ 圖3:使用Mock進行單元測試

在圖3中,因為使用了Mock技術,測試類別方法使用到的相關類別物件都變成了對應的Mock物件,因為是Mock物件,所以可以定義其行為,例如:使Mock物件回傳需要的值。這解決了撰寫單元測試需要依賴外部系統(如資料庫)的問題,因為Mock物件可以自行定義其行為,以資料庫類別為例,資料庫類別的Mock物件可以完全不需要連線到資料庫,而只依照傳入的參數回傳特定的資料,如此一來,大大簡化了撰寫單元測試的複雜度,並且提高了單元測試的執行速度。也可以讓開發人員只需要專注在自己負責開發的類別與單元測試,提高程式碼的品質。

為何需要Mock

Mock對於撰寫單元測試還有哪些好處呢?以下是幾個撰寫單元測試需要Mock的原因:

●當無法(或者非常困難)建立真實類別物件進行測試時:
當真實類別物件是在執行時期才產生,例如:如果有個類別方法使用到ASP.Net的Session物件,對這種類別方法進行單元測試時就無法直接呼叫這個類別方法後驗證結果,因為這個類別方法使用到Session物件,直接呼叫會造成程式錯誤。如果要對此類別方法撰寫單元測試,必須將Session物件Mock後重新定義行為,單元測試才可以直接呼叫此方法並驗證其結果。

●當真實類別物件的行為不確定(non-deterministic)時:
當真實類別物件回傳值會隨時變動時,例如,假設有個類別方法使用到其他類別回傳的目前溫度值,而溫度是隨時會變動的值,如果需要對這個類別方法進行單元測試,必須將其他類別Mock後回傳定義好的溫度值才能夠驗證此類別方法是否正確。

●當真實類別物件的行為無法(或者非常困難)被觸發時:
例如:假設有個類別方法會呼叫web service連接到外部系統,而目前單元測試要測試當web service無法連線時是否能正確處理,在這個狀況下,若web service無法連線的行為不常發生,可將web service類別物件Mock後,重新定義其行為為無法連線(例如:拋出網路連線錯誤異常(exception))即可測試此類別方法。

●當真實類別物件的執行速度相當緩慢時:
例如:有個類別方法呼叫另一個讀取整個磁碟檔案系統的類別時,如果單元測試直接呼叫這個方法,執行時間會非常久,在這種情況下,可以將讀取整個磁碟檔案系統資料的類別Mock後,直接回傳相關定義好的資訊,然後進行單元測試,就可以大幅度縮短這個單元測試的執行時間。

●當真實類別物件有使用者介面時:
例如:有個類別方法要讀取某一個文字方塊的值後進行相關處理,若要對這種類別方法進行單元測試,必須將文字方塊物件Mock後回傳定義好的值給類別方法,如此就可以對此類別方法進行單元測試。

綜合以上可以發現:在實務上,撰寫單元測試必須要使用Mock才能夠正確驗證測試類別方法。

因為Mock本身牽涉到相當底層的技術,通常不建議自行撰寫Mock類別。現在在.Net Framework中有許多Mock的Library可以使用,在下篇文章中,筆者介紹一套個人認為目前功能最強大的Mock Library—Typemock Isolator。讀者可以從實際的程式碼範例中體會好的Mock Library對於單元測試的重要性。