前言

在工作中或者在面試中,我經(jīng)常碰到的開(kāi)發(fā)人員就是對(duì)單元測(cè)試不重視,這一類基本上都表現(xiàn)出了一種“無(wú)知的自信”,總覺(jué)得自己寫的代碼質(zhì)量很高,直到一次次蟲(chóng)子(Bug)把自己咬的頭破血流時(shí),才發(fā)現(xiàn)原來(lái)自己的代碼已經(jīng)到了剪不斷理還亂的狀態(tài),而每一次修改一個(gè)bug,都需要走一遍“墨鏡迷宮” (看上圖)。還有很多人知道單元測(cè)試或者寫出了單元測(cè)試,但是就是寫了一個(gè)方法,上面標(biāo)注了一個(gè)[Test]屬性而已,甚至很多的人單元測(cè)試上面標(biāo)注的是[IgnoreTest], 每次看見(jiàn)這些,我都深深的感到推行單元測(cè)試之路是艱難的,是遙遠(yuǎn)的,但是我依然堅(jiān)信是是渴望也可及的,只要有著深深的信念,堅(jiān)強(qiáng)的意志,無(wú)謂的勇氣,一頭扎進(jìn)去泥巴堆里,假以時(shí)日,當(dāng)大雨來(lái)臨,必將帶走泥巴,從此你拔劍揚(yáng)眉,哦,你不用拔劍了,因?yàn)槟憔褪莿?。?!?/p>
為了讓更多人能夠拔劍揚(yáng)眉,也為了我們公司剛?cè)肼毜男氯俗鲆恍┡嘤?xùn),我精心準(zhǔn)備了單元測(cè)試的一些知識(shí),在此為你奉上,我盡量用簡(jiǎn)短的語(yǔ)句來(lái)描述,如果你不清楚我說(shuō)的某一些點(diǎn),那么歡迎你發(fā)郵件給我 wangdeshui@outlook.com,我可以針對(duì)集中的點(diǎn)發(fā)篇文章,如果你想知道我說(shuō)的所有點(diǎn)怎么實(shí)踐,那就聯(lián)系我,試試加入到我們公司來(lái)。
什么是單元測(cè)試
單元測(cè)試是開(kāi)發(fā)者編寫的一小段代碼,用于檢驗(yàn)被測(cè)代碼中的一個(gè)很明確的功能是否正確。通常而言,一個(gè)單元測(cè)試是用于判斷某個(gè)特定條件(或者場(chǎng)景)下某個(gè)特定函數(shù)的行為。
執(zhí)行單元測(cè)試,是為了證明某段代碼的行為確實(shí)和開(kāi)發(fā)者所期望的一致。因此,我們所要測(cè)試的是規(guī)模很小的、非常獨(dú)立的功能片段。通過(guò)對(duì)所有單獨(dú)部分的行為建立起信心。然后,才能開(kāi)始測(cè)試整個(gè)系統(tǒng)。
為什么要使用單元測(cè)試
- 單元測(cè)試使工作完成的更輕松
- 單元測(cè)試使你的設(shè)計(jì)更好
- 大大減少花在調(diào)試上的時(shí)間
- 能幫助你更好的理解代碼
沒(méi)有單元測(cè)試
- 任何代碼都是在假定其他代碼是正確無(wú)誤的情況下編寫的。
- 修改一處代碼時(shí)無(wú)法得知會(huì)對(duì)其他代碼產(chǎn)生怎樣的影響。
- 任何一處改動(dòng)都需要進(jìn)行功能級(jí)別的整體調(diào)試。
單元測(cè)試難以推動(dòng)的原因
太花時(shí)間
很多人認(rèn)為單元測(cè)試很花時(shí)間,但是想想我們?cè)谙旅鎺c(diǎn)話的時(shí)間,我經(jīng)常看到為了測(cè)試一個(gè)簡(jiǎn)單的API方法,我們很多人必須讓前端跑起來(lái),甚至自己寫一個(gè)客戶端才能調(diào)用
- 調(diào)試上花的時(shí)間
- 對(duì)自認(rèn)為正確的代碼,花了多少時(shí)間確認(rèn)代碼是正確的。
- 定位Bug所耗的時(shí)間
測(cè)試不是我的工作
很多人認(rèn)為測(cè)試不是自己的工作,但是想一想每次測(cè)試提出一個(gè)bug所花的時(shí)間,以及你改bug所化的時(shí)間,所以下面2點(diǎn)是很重要
- 內(nèi)在質(zhì)量的重要性
- 測(cè)試應(yīng)該是輔助,好的軟件是開(kāi)發(fā)設(shè)計(jì)出來(lái)的,不是測(cè)試出來(lái)的
系統(tǒng)可測(cè)試性差
- 系統(tǒng)耦合度很高,我們需要提高我們的團(tuán)隊(duì)的設(shè)計(jì)能力。
單元測(cè)試最佳實(shí)踐
實(shí)踐一: 三到五步
- SetUp
- 輸入
- 調(diào)用
- 輸出
- TearDown
實(shí)踐二: 運(yùn)行快速
為什么?
單元測(cè)試運(yùn)行很頻繁,是輔助開(kāi)發(fā)的,在開(kāi)發(fā)過(guò)程中運(yùn)行,如果慢影響很大
多快較好?
- 單個(gè)測(cè)試小于200ms
- 單個(gè)測(cè)試套件小于10s
- 整個(gè)測(cè)試小于10分鐘
實(shí)踐三:一致性
任何時(shí)候同樣的輸入需要同樣的結(jié)果
Date date=new Date()
Random.next()
這樣的代碼都需要Mock掉,不然時(shí)間每次都不同,結(jié)果就會(huì)不一樣。
實(shí)踐四:原子性
** 所有的測(cè)試只有兩種結(jié)果:成功和失敗**
不能部分測(cè)試通過(guò)
實(shí)踐五:?jiǎn)我宦氊?zé)
一個(gè)測(cè)試只驗(yàn)證一個(gè)行為
** 測(cè)試行為,不要測(cè)試方法 **
- 一個(gè)方法,多個(gè)行為 -----> 多個(gè)測(cè)試
- 一個(gè)行為,多個(gè)方法 ----- 一個(gè)測(cè)試
這里的一個(gè)行為,多個(gè)方法一般指這個(gè)方法調(diào)用private, protected, getters, setters - 多個(gè)Assert只有在測(cè)試同一個(gè)行為時(shí)可以接受
實(shí)踐六:獨(dú)立無(wú)耦合
單元測(cè)試之間無(wú)相互調(diào)用
- 單元測(cè)試執(zhí)行順序無(wú)關(guān)
- 不同的順序無(wú)影響
單元測(cè)試之間不能共享狀態(tài)
比如一個(gè)測(cè)試?yán)镌O(shè)置了一個(gè)屬性值,然后在另外一個(gè)測(cè)試?yán)镉茫绻仨毠蚕砜梢苑诺絊etup里
實(shí)踐七:隔離外部調(diào)用
- 單元測(cè)試需要快速運(yùn)行,且每次結(jié)果一致,所以需要隔離一切對(duì)外部的調(diào)用。
- 不使用具體的其它真實(shí)類,就是不要new
- 不讀數(shù)據(jù)庫(kù)
- 不讀網(wǎng)絡(luò)
- 不讀外部文件
- 適當(dāng)時(shí)候可以構(gòu)造一個(gè)相同的內(nèi)部文件來(lái)Mock
- 不依賴本地時(shí)間
- 不依賴環(huán)境變量
實(shí)踐八: 自描述
- 單元測(cè)試是開(kāi)發(fā)級(jí)文檔
-
單元測(cè)試是方法的描述
單元測(cè)試自描述命名
實(shí)踐九: 單元測(cè)試邏輯
- 單元測(cè)試必須容易讀和理解的
- 變量名,方法名,類名
- 無(wú)條件語(yǔ)句,無(wú)Switch
辦法:分解if到多個(gè)測(cè)試,所有的輸入都是已知的,所有的結(jié)果都是一定的(Mock) - 無(wú)循環(huán)語(yǔ)句
- 無(wú)異常捕捉
** 測(cè)試預(yù)知的異常,用ExpectedException方法 **
實(shí)踐十: 斷言
斷言信息最好包含Business Information
斷言信息包含出錯(cuò)的具體信息如果失敗
-
適當(dāng)時(shí)候可以封裝自己的Assert
比如:Assert.IsProgrammer(Jack) Return Jack. Cancooking() && Jack.CanCoding()
實(shí)踐十一:產(chǎn)品代碼
-
產(chǎn)品代碼無(wú)測(cè)試邏輯
不能有:If(global.IsTest){…} 測(cè)試代碼和產(chǎn)品代碼要分離
不要在產(chǎn)品代碼里有任何只供測(cè)試用的代碼
使用依賴注入
最后,單元測(cè)試常用技術(shù)及工具
下面是.NET程序常用的單元測(cè)試需要的技術(shù)和工具,其它語(yǔ)言請(qǐng)自信比對(duì)。
- 面向接口編程
- 依賴注入(Castle, Unity, Ninject)
- Moq
- 測(cè)試工具(xUnit)
- .Net Nunit
- 代碼覆蓋率測(cè)試工具Ncover
- 自動(dòng)運(yùn)行測(cè)試輔助工具NCrunch
