首先聲明以下大部分是摘錄。
原則定的都很好,是不是真的能做到?一切看起來都很美,一切聽起來都對,在做的時候是不是真的落實了?
先來講一個單元測試的故事
單元測試寫出來容易跑過難!而且跑不過的原因還不是你的開發代碼邏輯錯了,而是測試環境/數據出問題。要測試,一定要有數據,這個數據的構建,完全不是我們所想象的那么簡單。以我們項目里的積分系統為例,假設一個簡單的需求:博客被點贊,博客的作者應該獲得一定積分,該積分數量是由點贊人目前所有的可用幣轉換而得來的。要准備的數據就有:博客一篇,要有作者,作者已有積分;點贊人一名,有一定數量可用幣。如果只是這樣,還可以接受,但其實下面會有一堆的問題:
- 作者的積分從哪里來?我們的開發代碼,出於封裝的考慮,用戶的積分是只讀的,你單元測試怎么設這個值?
- 要么寫代碼,模擬作者通過其他行為(發布文章回答問題等)獲得積分,這將開啟新一輪噩夢;
- 如果用Mock或者反射強行設置,事實上省略了作者獲得積分的歷史,所以用戶“積分歷史”為null,之后對其“加積分”時,就會報異常。
- 更坑的是,你以為你什么都處理好了的時候,你突然悲哀的發現,這個博客得首先“被發布”,而博客一經發布,其作者就獲得了一定數量的積分,所以你以前設置的積分又變了!
- ……
- 點贊人的可用幣,同樣可能遇到類似的問題。可用幣怎么設置,設置之后會不會在跑測試時被意外更改?
- 點贊的行為,被封裝成一個方法,運行這個方法,會檢查點贊人之前是否已經對該文章點過贊,所以還應該有一個“點贊歷史記錄”,哪怕是空的,都得new一個,否則就空異常
- ……
反正當時是寫得我直接摔了鼠標!寫得憋屈啊!而且我還是完全隔絕了數據庫的,真不知道那些要從數據庫里取數據來跑單元測試的,是怎么做的?這時候我一下子就明白了,實際工作中加班趕進度,一個接一個的填坑,連重構的時間都沒有,怎么可能還擠得出時間來寫單元測試?就算開始雄心勃勃的寫了,隨着系統日益復雜,維護單元測試的成本也與日俱增,甚至復雜度更甚開發,所以放棄也就成了絕大多數項目的唯一選擇。
這個故事聽起來就是一個從入門到放棄的例子。有很多的細節點都值得深思。
- 如何對某些模塊獨立測試,屏蔽相關項?
- 數據庫操作怎么屏蔽?
- 變更需求測試代碼也變更,造成大量工作量
所以,今天來講講單元測試的相關基礎知識及原則,自己對於單元測試的理解。
單元測試是什么?
單元測試是針對軟件的最小模塊進行正確性檢驗的測試工作。
什么是測試用例?
A test case has components that describes an input, action or event and an expected response, to determine if a feature of an application is working correctly.
哪些代碼需要做單元測試?
所有的代碼都需要單元測試.
所有公共(public)的代碼都需要單元測試.
單元測試是程序員還是測試負責?
單元測試是由程序員自己來完成,最終受益的也是程序員自己。可以這么說,程序員有責任編寫功能代碼,同時也就有責任為自己的代碼編寫單元測試。執行單元測試,就是為了證明這段代碼的行為和我們期望的一致。
為什么要使用單元測試
單元測試會為我們的承諾做保證。編寫單元測試就是用來驗證這段代碼的行為是否與我們期望的一致。有了單元測試,我們可以自信的交付自己的代碼,而沒有任何的后顧之憂。
- 單元測試使工作完成的更輕松
- 單元測試使你的設計更好
- 大大減少花在調試上的時間
- 能幫助你更好的理解代碼
如果沒有單元測試
- 任何代碼都是在假定其他代碼是正確無誤的情況下編寫的。
- 修改一處代碼時無法得知會對其他代碼產生怎樣的影響。
- 任何一處改動都需要進行功能級別的整體調試。
什么是Mock,Stub,Spy?
(sinon.js對以下概念的定義)
Spy的作用在於可以監視一個函數的被調用情況。函數會被實際調用,Spy相當於給函數加了一層wrapper,可以記錄函數被調用了幾次,每次的參數是什么,每次返回結果是什么,出了什么異常。
Stub的作用是在測試中遇到這樣的情形:測試函數f1,f1依賴於函數f2,根據最小化測試粒度的原則,測試f1時不要帶入f2的測試,那么我們要確保f2的輸出是正確的,所以直接讓f2返回正確值的方法就叫stub。測試時,stub的方法是不會被具體執行的。
Mock是對於一個對象的監視,並且需要定義對這個對象的具體期待(verify),然后驗證這些期待是否和測試數據一致。
在sinon.js等測試工具中,mock對象是不會被真正執行的。spy是真正執行的。
在其他一些測試工具中,對以上概念有所變化和混淆。例如mockito中,mock對象后,具體方法也可以定義返回值,作用等同於stub。
單元測試的原則
1、每次只對一個對象進行UT測試(unit-test one object at a time)。這樣能使你盡快發現問題,而不被各個對象之間的復雜關系所迷惑。
2、給測試方法起個好名字(choose meaningful test method names)。應該是用形如testXXXYYY(),這樣的格式來命名你的測試方法。前綴test是Junit查找測試方法的依據,XXX應該是你測試的方法名,YYY應該是你測試的狀態。當然如果你只有一種狀態需要測試可以直接命名為testXXX()。
3、明確寫出出錯原因(explain the failure reason in assert calls)。在使用assertTrue,assertFalse,assertNotNull,assertNull方法時,應該將可能的錯誤的描述字符串,以第一個參數傳入相應的方法。這樣你可以迅速的找出出錯原因。
4、一個UT測試方法只應該測試一種情況(one unit test equals one testMethod)。一個方法中的多次測試,只會混亂你的測試目的。
5、測試任何可能的錯誤(test anything that could possibly fail)。你的測試代碼不是為了證明你是對的,而是為了證明你沒有錯。因此對測試的范圍要全面,比如邊界值、正常值、錯誤值;對代碼可能出現的問題要全面預測。
6、讓你的測試幫助改善你的代碼(let the test improve the code)。測試代碼永遠是我們代碼的第一個用戶,所以不僅讓他幫組我們發現Bug,還要幫組我們改善我們的設計,就是有名的測試驅動開發(Test-Driven Development,TDD)。
7、一樣的包,不同的位置(same package, separate directories)。測試的代碼和被測試的代碼應該放到不同的文件夾中,建議使用這種目錄 src/java/代碼 src/test/測試代碼。這樣可以讓兩份代碼使用一樣的包結構,但是放在不同的目錄下。
8、關於setup與teardown
a) 不要用TestCase的構造函數初始化Fixture,而要用setUp()和tearDown()方法。
c) 當繼承一個測試類時,記得調用父類的setUp()和tearDown()方法。
9、不要在mock object中牽扯到業務邏輯(don’t write business logic in mock objects)。
10、只對可能產生錯誤的地方進行測試(only test what can possibly break)。如:一個類中頻繁改動的函數。對於那些僅僅只含有getter/setter的類,如果是由IDE(如Eclipse)產生的,則可不測;如果是人工寫,那么最好測試一下。
11、盡量不要依賴或假定測試運行的順序,因為JUnit利用Vector保存測試方法。所以不同的平台會按不同的順序從Vector中取出測試方法。
12、避免編寫有副作用的TestCase,你要確信保持你的測試方法之間是獨立的。
13、將測試代碼和工作代碼放在一起,一邊同步編譯和更新(使用Ant中有支持junit的task)。
14、確保測試與時間無關,不要依賴使用過期的數據進行測試。導致在隨后的維護過程中很難重現測試。
15、如果你編寫的軟件面向國際市場,編寫測試時要考慮國際化的因素。不要僅用母語的Locale進行測試。
16、盡可能地利用JUnit提供地assert/fail方法以及異常處理的方法,可以使代碼更為簡潔。
17、測試要盡可能地小,執行速度快。
單元測試最佳實踐
實踐一: 三到五步
- SetUp
- 輸入
- 調用
- 輸出
- TearDown
實踐二: 運行快速
為什么?
單元測試運行很頻繁,是輔助開發的,在開發過程中運行,如果慢影響很大
多快較好?
- 單個測試小於200ms
- 單個測試套件小於10s
- 整個測試小於10分鍾
實踐三:一致性
任何時候同樣的輸入需要同樣的結果
Date date=new Date() Random.next()
這樣的代碼都需要Mock掉,不然時間每次都不同,結果就會不一樣。
實踐四:原子性
** 所有的測試只有兩種結果:成功和失敗**
不能部分測試通過
實踐五:單一職責
一個測試只驗證一個行為
** 測試行為,不要測試方法 **
- 一個方法,多個行為 -----> 多個測試
- 一個行為,多個方法 ----- 一個測試
這里的一個行為,多個方法一般指這個方法調用private, protected, getters, setters - 多個Assert只有在測試同一個行為時可以接受
實踐六:獨立無耦合
單元測試之間無相互調用
- 單元測試執行順序無關
- 不同的順序無影響
單元測試之間不能共享狀態
比如一個測試里設置了一個屬性值,然后在另外一個測試里用,如果必須共享可以放到Setup里
實踐七:隔離外部調用
- 單元測試需要快速運行,且每次結果一致,所以需要隔離一切對外部的調用。
- 不使用具體的其它真實類,就是不要new
- 不讀數據庫
- 不讀網絡
- 不讀外部文件
- 適當時候可以構造一個相同的內部文件來Mock
- 不依賴本地時間
- 不依賴環境變量
實踐八: 自描述
- 單元測試是開發級文檔
- 單元測試是方法的描述
實踐九: 單元測試邏輯
- 單元測試必須容易讀和理解的
- 變量名,方法名,類名
- 無條件語句,無Switch
辦法:分解if到多個測試,所有的輸入都是已知的,所有的結果都是一定的(Mock) - 無循環語句
- 無異常捕捉
** 測試預知的異常,用ExpectedException方法 **
實踐十: 斷言
- 斷言信息最好包含Business Information
- 斷言信息包含出錯的具體信息如果失敗
-
適當時候可以封裝自己的Assert
比如:Assert.IsProgrammer(Jack)
Return Jack. Cancooking() && Jack.CanCoding()
實踐十一:產品代碼
- 產品代碼無測試邏輯
不能有:
If(global.IsTest){…}
- 測試代碼和產品代碼要分離
- 不要在產品代碼里有任何只供測試用的代碼
- 使用依賴注入