若是你對以上問題很是熟悉,那麼我想你的團隊和咱們遇到了相同的問題。html
WWH:Why,What,How爲何要作單元測試,什麼事單元測試,如何作單元測試。數據庫
一門技術或一個解決方案的誕生的誕生,不可能憑空去創造,每每是問題而催生出來的。在個人.NET持續集成與自動化部署之路第一篇(半天搭建你的Jenkins持續集成與自動化部署系統)這篇文章中提到,我在作研發負責人的時候飽受深夜加班上線之苦,其中提到的兩個大問題一個是部署問題,另外一個就是測試問題。部署問題,咱們引入了自動化的部署(後來咱們作到了幾分鐘就能夠上線)。咱們要作持續集成,剩下的就是測試問題了。編程
迴歸測試成了咱們的第一大問題。隨着咱們項目的規模與複雜度的提高,咱們的迴歸測試變得愈來愈困難。因爲咱們的當時的測試全依賴手工測試,咱們項目的迭代週期大概在一個月左右,而測試的時間就要花費一半多的時間。甚至版本上線之後作一遍迴歸測試就須要幾個小時的時間。並且這種手工進行的功能性測試很容易有遺漏的地方,所以線上Bug層出不窮。一堆問題困擾着咱們,咱們不得不考慮進行自動化的測試。json
自動化測試一樣不是銀彈,自動化測試雖然與手工測試相比有其優勢,其測試效率高,資源利用率高(通常白天開發寫用例,晚上自動化程序跑),能夠進行壓力、負載、併發、重複等人力不可完成的測試任務,執行效率較快,執行可靠性較高,測試腳本可重複利用,bug及時發現.......但也有其不可避免的缺點,如:只適合迴歸測試,開發中的功能或者變動頻繁的功能,因爲變動頻繁而不斷更改測試腳本是不划算的,而且腳本的開發也須要高水平的測試人員和時間......整體來講,雖然自動化的測試能夠解決一部分的問題,但也一樣會帶來另外一些問題。到底應該不該該引入自動化的測試還須要結合本身公司的團隊現狀來綜合考慮。c#
而咱們的團隊從短時間來看引入自動化的測試其必然會帶來一些問題,但長遠來看其優勢仍是要大於其缺陷的,所以咱們決定作自動化的測試,固然這具體是否是另外一個火坑還須要時間來斷定!瀏覽器
以上即是經典的自動化測試金字塔。網絡
位於金字塔頂端的是探索性測試,探索性測試並無具體的測試方法,一般是團隊成員基於對系統的理解,以及基於現有測試沒法覆蓋的部分,作出系統性的驗證,譬如:跨瀏覽器的測試,一些視覺效果的測試等。探索性測試因爲這類功能變動比較頻繁,並且所有實現自動化成本較高,所以小範圍的自動化的測試仍是有效的。並且其強調測試人員的主觀能動性,也不太容易經過自動化的測試來實現,更多的是手工來完成。所以其成本最高,難度最大,反饋週期也是最慢的。併發
而在測試金字塔的底部是單元測試,單元測試是針對程序單元的檢測,一般單元測試都能經過自動化的方式運行,單元測試的實現成本較低,運行效率較高,可以經過工具或腳本徹底自動化的運行,此外,單元測試的反饋週期也是最快的,當單元測試失敗後,可以很快發現,而且可以較容易的找到出錯的地方並修正。重要的事單元測試通常由開發人員編寫完成。(這一點很重要,由於在我這個二線小城市裏,可以編寫代碼的測試人員實在是罕見!)框架
在金字塔的中間部分,自底向上還包括接口(契約)測試,集成測試,組件測試以及端到端測試等,這些測試側重點不一樣,所使用的技術方法工具等也不相同。ide
整體而言,在測試金字塔中,從底部到頂部業務價值的比重逐漸增長,即越頂部的測試其業務價值越大,但其成本也愈來愈大,而越底部的測試其業務價值雖小,但其成本較低,反饋週期較短,效率也更高。
咱們要開始作自動化測試,但不可能一會兒全都作(考慮咱們的人力與技能也作不到)。所以必須有側重點,考慮良久最終咱們決定從單元測試開始。因而我在剛吃了自動化部署的螃蟹以後,不得不來吃自動化測試的第一個螃蟹。既然決定要作,那麼咱們就要先明白單元測試是什麼?
咱們先來看幾個常見的對單元測試的定義。
用最簡單的話說:單元測試就是針對一個工做單元設計的測試,這裏的「工做單元」是指對一個工做方法的要求。
單元測試是開發者編寫的一小段代碼,用於檢測被測代碼的一個很小的、很明確的功能是否正確。一般而言,一個單元測試用於判斷某個特定條件(或場景)下某個特定函數的行爲。
例:
你可能把一個很大的值放入一個有序list中去,而後確認該值出如今list的尾部。或者,你可能會從字符串中刪除匹配某種模式的字符,而後確認字符串確實再也不包含這些字符了。
執行單元測試,就是爲了證實某段代碼的行爲和開發者所指望的一致!
這裏咱們暫且先將其分爲三種狀況
單元測試背後的思想是,僅測試這個方法中的內容,測試失敗時不但願必須穿過基層代碼、數據庫表或者第三方產品的文檔去尋找可能的答案!
當測試開始滲透到其餘類、服務或系統時,此時測試便跨越了邊界,失敗時會很難找到缺陷的代碼。
測試跨邊界時還會產生另外一個問題,當邊界是一個共享資源時,如數據庫。與團隊的其餘開發人員共享資源時,可能會污染他們的測試結果!
若是發現所編寫的測試對一件以上的事情進行了測試,就可能違反了「單一職責原則」。從單元測試的角度來看,這意味着這些測試是難以理解的非針對性測試。隨着時間的推移,向類或方法種添加了更多的不恰當的功能後,這些測試可能會變的很是脆弱。診斷問題也將變得極具備挑戰性。
如:StringUtility中計算一個特定字符在字符串中出現的次數,它沒有說明這個字符在字符串中處於什麼位置也沒有說明除了這個字符出現多少次以外的其餘任何信息,那麼這些功能就應該由StringUtility類的其它方法提供!一樣,StringUtility類也不該該處理數字、日期或複雜數據類型的功能!
2.2.3 不可預測的測試
單元測試應當是可預測的。在針對一組給定的輸入參數調用一個類的方法時,其結果應當老是一致的。有時,這一原則可能看起來很難遵照。例如:正在編寫一個日用品交易程序,黃金的價格可能上午九時是一個值,14時就會變成另外一個值。
好的設計原則就是將不可預測的數據的功能抽象到一個能夠在單元測試中模擬(Mock)的類或方法中(關於Mock請往下看)。
在單元測試框架出現以前,開發人員在建立可執行測試時飽受折磨。最初的作法是在應用程序中建立一個窗口,配有"測試控制工具(harness)"。它只是一個窗口,每一個測試對應一個按鈕。這些測試的結果要麼是一個消息框,要麼是直接在窗體自己給出某種顯示結果。因爲每一個測試都須要一個按鈕,因此這些窗口很快就會變得擁擠、不可管理。
因爲人們編寫的大多數單元測試都有很是簡單的模式:
執行一些簡單的操做以創建測試。
執行測試。
驗證結果。
必要時重設環境。
因而,單元測試框架應運而生(實際上就像咱們的代碼優化中提取公共方法造成組件)。
單元測試框架(如NUnit)但願可以提供這些功能。單元測試框架提供了一種統一的編程模型,能夠將測試定義爲一些簡單的類,這些類中的方法能夠調用但願測試的應用程序代碼。開發人員不須要編寫本身的測試控制工具;單元測試框架提供了測試運行程序(runner),只須要單擊按鈕就能夠執行全部測試。利用單元測試框架,能夠很輕鬆地插入、設置和分解有關測試的功能。測試失敗時,測試運行程序能夠提供有關失敗的信息,包含任何可供利用的異常信息和堆棧跟蹤。
.Net平臺經常使用的單元測試框架有:MSTesting、Nunit、Xunit等。
/// <summary> /// 計算器類 /// </summary> public class Calculator { /// <summary> /// 加法 /// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <returns></returns> public double Add(double a, double b) { return a + b; } /// <summary> /// 減法 /// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <returns></returns> public double Sub(double a, double b) { return a - b; } /// <summary> /// 乘法 /// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <returns></returns> public double Mutiply(double a, double b) { return a * b; } /// <summary> /// 除法 /// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <returns></returns> public double Divide(double a, double b) { return a / b; } }
/// <summary> /// 針對計算加減乘除的簡單的單元測試類 /// </summary> [TestFixture] public class CalculatorTest { /// <summary> /// 計算器類對象 /// </summary> public Calculator Calculator { get; set; } /// <summary> /// 參數1 /// </summary> public double NumA { get; set; } /// <summary> /// 參數2 /// </summary> public double NumB { get; set; } /// <summary> /// 初始化 /// </summary> [SetUp] public void SetUp() { NumA = 10; NumB = 20; Calculator = new Calculator(); } /// <summary> /// 測試加法 /// </summary> [Test] public void TestAdd() { double result = Calculator.Add(NumA, NumB); Assert.AreEqual(result, 30); } /// <summary> /// 測試減法 /// </summary> [Test] public void TestSub() { double result = Calculator.Sub(NumA, NumB); Assert.LessOrEqual(result, 0); } /// <summary> /// 測試乘法 /// </summary> [Test] public void TestMutiply() { double result = Calculator.Mutiply(NumA, NumB); Assert.GreaterOrEqual(result, 200); } /// <summary> /// 測試除法 /// </summary> [Test] public void TestDivide() { double result = Calculator.Divide(NumA, NumB); Assert.IsTrue(0.5 == result); } }
單元測試是很是有魔力的魔法,可是若是使用不恰當亦會浪費大量的時間在維護和調試上從而影響代碼和整個項目。
好的單元測試應該具備如下品質:
• 自動化
• 完全的
• 可重複的
• 獨立的
• 專業的
通常來講有六個值得測試的具體方面,能夠把這六個方面統稱爲Right-BICEP:
代碼中的許多Bug常常出如今邊界條件附近,咱們對於邊界條件的測試該如何考慮?
單元測試的目標是一次只驗證一個方法或一個類,可是若是這個方法依賴一些其餘難以操控的東西,好比網絡、數據庫等。這時咱們就要使用mock對象,使得在運行unit test的時候使用的那些難以操控的東西其實是咱們mock的對象,而咱們mock的對象則能夠按照咱們的意願返回一些值用於測試。通俗來說,Mock對象就是真實對象在咱們調試期間的測試品。
Mock對象建立的步驟:
使用一個接口來描述這個對象。
爲產品代碼實現這個接口。
以測試爲目的,在mock對象中實現這個接口。
Mock對象示例:
/// <summary> ///帳戶操做類 /// </summary> public class AccountService { /// <summary> /// 接口地址 /// </summary> public string Url { get; set; } /// <summary> /// Http請求幫助類 /// </summary> public IHttpHelper HttpHelper { get; set; } /// <summary> /// 構造函數 /// </summary> /// <param name="httpHelper"></param> public AccountService(IHttpHelper httpHelper) { HttpHelper = httpHelper; } #region 支付 /// <summary> /// 支付 /// </summary> /// <param name="json">支付報文</param> /// <param name="tranAmt">金額</param> /// <returns></returns> public bool Pay(string json) { var result = HttpHelper.Post(json, Url); if (result == "SUCCESS")//這是咱們要測試的業務邏輯 { return true; } return false; } #endregion #region 查詢餘額 /// <summary> /// 查詢餘額 /// </summary> /// <param name="account"></param> /// <returns></returns> public decimal? QueryAmt(string account) { var url = string.Format("{0}?account={1}", Url, account); var result = HttpHelper.Get(url); if (!string.IsNullOrEmpty(result))//這是咱們要測試的業務邏輯 { return decimal.Parse(result); } return null; } #endregion }
/// <summary> /// Http請求接口 /// </summary> public interface IHttpHelper { string Post(string json, string url); string Get(string url); }
/// <summary> /// HttpHelper /// </summary> public class HttpHelper:IHttpHelper { public string Post(string json, string url) { //假設這是真實的Http請求 var result = string.Empty; return result; } public string Get(string url) { //假設這是真實的Http請求 var result = string.Empty; return result; } }
/// <summary> /// Mock的 HttpHelper /// </summary> public class MockHttpHelper:IHttpHelper { public string Post(string json, string url) { //這是Mock的Http請求 var result = "SUCCESS"; return result; } public string Get(string url) { //這是Mock的Http請求 var result = "0.01"; return result; } }
如上,咱們的AccountService的業務邏輯依賴於外部對象Http請求的返回值在真實的業務中咱們給AccountService注入真實的HttpHelper類,而在單元測試中咱們注入本身Mock的HttpHelper,咱們能夠根據不一樣的用例來模擬不一樣的Http請求的返回值來測試咱們的AccountService的業務邏輯。
注意:記住,咱們要測試的是AccountService的業務邏輯:根據不一樣http的請求(或傳入不一樣的參數)而返回不一樣的結果,必定要弄明白本身要測的是什麼!而無關的外部對象內的邏輯咱們並不關心,咱們只須要讓它給咱們返回咱們想要的值,來驗證咱們的業務邏輯便可
關於Mock對象通常會使用Mock框架,關於Mock框架的使用,咱們將在下一篇文章中介紹。.net 平臺經常使用的Mock框架有Moq,PhinoMocks,FakeItEasy等。
在作單元測試時,代碼覆蓋率經常被拿來做爲衡量測試好壞的指標,甚至,用代碼覆蓋率來考覈測試任務完成狀況,好比,代碼覆蓋率必須達到80%或90%。因而乎,測試人員費盡心思設計案例覆蓋代碼。所以我認爲用代碼覆蓋率來衡量是不合適的,咱們最根本的目的是爲了提升咱們迴歸測試的效率,項目的質量不是嗎?
本篇文章主要介紹了單元測試的WWH,分享了咱們爲何要作單元測試並簡單介紹了單元測試的概念以及如何去作單元測試。固然,千萬不要天真的覺得看了本篇文章就能作好單元測試,若是你的組織開始推動了單元測試,那麼在推動的過程當中相信仍然會遇到許多問題(就像咱們遇到的,依賴外部對象問題,靜態方法如何mock......)。如何更好的去作單元測試任重而道遠。下一篇文章將針對咱們具體實施推動單元測試中遇到的一些問題,來討論如何更好的作單元測試。如:如何破除依賴,如何編寫可靠可維護的測試,以及如何面向測試進行程序的設計等。
未完待續,敬請關注......
《單元測試的藝術》
咱們作單元測試的經歷