1.1單元測試的定義
單元測試就是針對一個工作單元設計的測試,這里的“工作單元”是指對一個工作方法的要求。
單元測試是開發者編寫的一小段代碼,用於檢測被測代碼的一個很小的、很明確的功能是否正確。通常而言,一個單元測試用於判斷某個特定條件(或場景)下某個特定函數的行為。
例:
你可能把一個很大的值放入一個有序list中去,然后確認該值出現在list的尾部。或者,你可能會從字符串中刪除匹配某種模式的字符,然后確認字符串確實不再包含這些字符了。
執行單元測試,就是為了證明某段代碼的行為和開發者所期望的一致!
//被測方法
public double Add(double a, double b)
{
return a + b;
}
//測試方法
[Test]
public void AddTest()
{
double result = new Calculator().Add(14, 15);
Assert.AreEqual(30,result);
}
1.2工作單元
調用系統的一個公共方法到產生一個測試可見的最終結果,其間這個系統發生的行為總稱為一個工作單元。我們通過系統的公共AP和行為就可以觀察到一個可見的最終結果,無需查看系統的內部狀態。一個最終結果可以是以下任何一種形式。
- 被調用的公共方法回一個值(一個返回值不為空的函數)
- 在方法調用的前后,系統的狀態或行為有可見的變化,這種變化無需查詢私有狀態即可判斷。(例如:一個以前不存在的用戶可以登入系統,或者一個狀態機系統的屬性發生變化。)
- 調用了一個不受測試控制的第三方系統,這個第三方系統不返回任何值,或者返回值都被忽略。(例如:調用一個第三方日志系統,這個系統不是你編寫的,而且你也沒有源代碼。)
很多人覺得被測試的工作單元應該盡可能的小。我卻不這么看,我認為工作單元這個概念意味着一個單元既可以小到只包含一個方法,也可以大到包括實現某個功能的多個類和函數。如果你的工作單元很大,卻但是其最終結果對用戶可見度高,易於維護也未嘗不是好的測試,相反如果試圖把工作單元縮到最小,最后會不得不偽造一堆東西反而會增加測試的復雜度,適得其反。
2.什么不是單元測試
單元測試其實是一門很基礎也很簡單的技術,然而在單元測試實踐過程中,往往會對單元測試產生一些誤區,進而寫出一些不是單元測試的"單元測試" ,其中常見的主要有以下三種。
2.1 跨邊界的測試
單元測試背后的思想是,僅測試這個方法中的內容,測試失敗時不希望必須穿過基層代碼、數據庫表或者第三方產品的文檔去尋找可能的答案!
當測試開始滲透到其他類、服務或系統時,此時測試便跨越了邊界,失敗時會很難找到缺陷的代碼。
測試跨邊界時還會產生另一個問題,當邊界是一個共享資源時,如數據庫。與團隊的其他開發人員共享資源時,可能會污染他們的測試結果!
2.2 不具有針對性的測試
如果發現所編寫的測試對一件以上的事情進行了測試,就可能違反了“單一職責原則”。從單元測試的角度來看,這意味着這些測試是難以理解的非針對性測試。隨着時間的推移,向類或方法種添加了更多的不恰當的功能后,這些測試可能會變的非常脆弱。診斷問題也將變得極具有挑戰性。
如:StringUtility中計算一個特定字符在字符串中出現的次數,它沒有說明這個字符在字符串中處於什么位置也沒有說明除了這個字符出現多少次之外的其他任何信息,那么這些功能就應該由StringUtility類的其它方法提供!同樣,StringUtility類也不應該處理數字、日期或復雜數據類型的功能!
2.3 不可預測的測試
單元測試應當是可預測的。在針對一組給定的輸入參數調用一個類的方法時,其結果應當總是一致的。有時,這一原則可能看起來很難遵守。例如:正在編寫一個日用品交易程序,黃金的價格可能上午九時是一個值,14時就會變成另一個值。
而好的設計原則就是將不可預測的數據的功能抽象到一個可以在單元測試中模擬(Mock)的類或方法中
2.4 集成測試
其實上面三種測試已經到了集成測試的領域。任何測試,如果它運行速度不快,結果不穩定,或者要用到被測試單元的一個或多個真實依賴物,我們就認為它是集成測試。
集成測試是對一個工作單元進行的測試,這個測試對被測試的工作單元沒有完全的控制,並使用該單元的一個或多個真實依賴物,例如時間、網絡、數據庫、線程或隨機數產生器等。
集成測試本身並不是一種壞事,反而其具有和單元測試一樣高的地位,但是在實踐過程中我們把集成測試和單元測試分離開來還是很重要的。
3.優秀的單元測試有哪些特性
單元測試是非常有魔力的魔法,也是一把雙刃劍。使用得當,可以很有效的提高我們的編碼質量,提升研發效率,但是如果使用不恰當亦會浪費大量的時間在測試編碼、維護和調試上從而影響代碼和整個項目,徒勞而無功!
因此做好單元測試至關重要!而想要做好單元測試,我們首先應該知道優秀的單元測試有哪些特性。
一個好的單元測試一定是有以下幾個特性的
• 自動化
• 徹底的
• 可重復的
• 獨立的
• 專業的
回顧一下自己以前寫過的單元測試問自己幾個問題。
-
它是不是可以自動化一鍵運行、並且可以重復運行
-
幾個月后它是不是仍可以運行、並且得到期望的結果
-
它是否可以在幾分鍾內運行結束
-
在運行之前你是否不需要需要進行一系列的配置
-
每次運行是否能夠得到相同的結果
-
外部的系統因素是否不會影響你的測試結果
-
測試代碼是否很簡單就可以編寫完成
如果針對以上問題有任何一個的回答是“否”,那么你應該好好的思考一下到底如何去做好單元測試。
4. 如何進行單元測試
對於一個方法或者類,乍一看就能找出其隱藏深處的bug是很不容易的,因此在bug挖掘方面通常會有一些經驗和套路,來指導我們更好的進行單元測試。
3.1 測試哪些內容
一般來說有六個值得測試的具體方面,可以把這六個方面統稱為Right-BICEP:
- Right——結果
對於單元測試測試而言,首要的也是最明顯的任務就是查看所期望的結果是否正確,例如判斷一個方法的返回值是否為序列中的最大值...... - B——邊界條件
找邊界條件是做單元測試中最有價值的工作之一,因為bug一般就出現在邊界上。關於邊界條件2會有詳細總結 - I——檢查反向關聯
對於一些方法,我們可以使用反向的邏輯關系來驗證它們。例如,你可以用對結果進行平方的方式來檢查一個計算平方根的函數,然后測試結果是否和原數據很接近 - C——交叉檢查
有些時候我們實現一個問題會有不同的算法,在生產系統中我們使用一種算法,而在測試中我們可以使用另一種算法來驗證其結果是否一致。 - E——強制產生錯誤條件
在實際運行過程中,有時候會發生一些意外的難以避免的錯誤,例如磁盤會滿,網絡連線會斷開.....從而導致程序崩潰。我們應該在測試中強制引發錯誤,來測試代碼是否能夠按照預期處理這些異常。 - P——是否滿足性能條件
性能同樣是我們測試過程中需要驗證的指標
3.2 注意邊界條件
代碼中的許多Bug經常出現在邊界條件附近,對於邊界條件的測試我們可以從CORRECT七個方面進行考慮
- 一致性----值是否滿足預期的格式
- 有序性----一組值是否滿足預期的排序要求
- 區間性----值是否在一個合理的最大值最小值范圍內
- 引用、耦合性----代碼是否引用了一些不受代碼本身直接控制的外部因素
- 存在性----值是否存在(例如:非Null,非零,存在於某個集合中)
- 基數性----是否恰好具有足夠的值
- 時間性----所有事情是否都按照順序發生的?是否在正確的時間、是否及時
3.3 使用Mock對象
單元測試的目標是驗證我們的工作單元,但是如果這個工作單元依賴一些其他的對象或是一些難以操控的東西,比如網絡、數據庫等。這時我們就要使用mock對象,使得在運行UT的時候使用的那些難以操控的東西實際上是我們mock的對象,而我們mock的對象則可以按照我們的意願返回一些值用於測試。通俗來講,Mock對象就是真實對象在我們調試期間的測試品。對於外部對象內的邏輯我們並不關心,我們只需要讓它給我們返回我們想要的值,來驗證我們的業務邏輯即可
IFileExtensionManager fileManager;
public bool IsValidFileName(){
//獲取文件擴展名
string extName=fileManager.GetExtName();
if(extName=="jpg"){
return true;
}
return false;
}
如上示例,假設從文件系統中讀取一個文件,獲取文件的擴展名,如果擴展名是jpg就返回true,否則返回false。
注意,這里我們要測試的邏輯是如果擴展名是jpg就返回true,否則返回false。而對於fileManager.GetExtName()方法內部的邏輯是什么樣的的我們是不關心的,我們只需要mock這個方法使其返回我們想要的值就可以了。
關於具體如何去mock工作單元中的一些外部依賴,會在存根與模擬對象里面詳細進行總結。
總結
本文總結了什么是單元測試、什么不是單元測試以及優秀的單元測試有哪些特性,簡單介紹了如何進行單元測試。
編寫差勁的單元測試是沒有意義的,我看到過很多公司嘗試去實踐單元測試,但最終要么在某個階段放棄了,要么並沒有真正執行單元測試。最終還是依賴集成測試或者人工測試來發現問題,不得不以失敗而告終,並堂而皇之的認為單元測試是一個耗時好力而無功的雞肋東西。
因此如果你想要真正的去實踐單元測試,那么必須充分的理解到底什么是單元測試,已經如何去更好的進行實踐優秀的單元測試。
而對於如何更好的去實踐單元測試,后續會結合實踐用更多的篇幅去總結分享。