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工作單元中的一些外部依賴,會在存根與模擬物件裡面詳細進行總結。
總結
本文總結了什麼是單元測試、什麼不是單元測試以及優秀的單元測試有哪些特性,簡單介紹瞭如何進行單元測試。
編寫差勁的單元測試是沒有意義的,我看到過很多公司嘗試去實踐單元測試,但最終要麼在某個階段放棄了,要麼並沒有真正執行單元測試。最終還是依賴整合測試或者人工測試來發現問題,不得不以失敗而告終,並堂而皇之的認為單元測試是一個耗時好力而無功的雞肋東西。
因此如果你想要真正的去實踐單元測試,那麼必須充分的理解到底什麼是單元測試,已經如何去更好的進行實踐優秀的單元測試。
而對於如何更好的去實踐單元測試,後續會結合實踐用更多的篇幅去總結分享。