說起單元測試,多數(shù)同學(xué)應(yīng)該都知道或聽過,可能不少同學(xué)認(rèn)為自己也寫過,甚至覺得單元測試很簡單有什么好培訓(xùn)的?其實(shí)這個事情還真沒想象的那么簡單!我基本可以比較負(fù)責(zé)任的說,你若沒深入對單元測試做過研究,不知道Mock對象為何物的話,那么可能你以前寫過的單元測試壓根就不是單元測試。
單元測試是什么?
這個問題其實(shí)并不太容易一兩句話說得特別清楚。先借用下百度百科的定義:
單元測試是在軟件開發(fā)過程中要進(jìn)行的最低級別的測試活動,在單元測試活動中,軟件的獨(dú)立單元將在與程序的其他部分相隔離的情況下進(jìn)行測試。
從以上這句定義我們可以看到,兩個提取到到兩個非常關(guān)鍵的字:最小粒度、隔離
● 單元測試是測試的最小單位,必須可信任的,可重復(fù)執(zhí)行的。
● 如果測試的范圍輕易的就會擴(kuò)展到其他類或同類的其他方法,那就不再是最小單位,也就不是單元測試了!
例如:
類A中的方法CallMethod中調(diào)用了類B中的方法DoMethod,如果在編寫測試的時候不把B類中的DoMethod隔離出來,造成單元測試CallMethod方法時,實(shí)際實(shí)行了DoMethod方法,那么這個測試方法不能算作是單元測試。(如何隔離會在后文詳解)
單元測試的目的是什么?
有人曾給我一段非常簡單的代碼片段:一個方法,里面只是調(diào)用若干個其他方法,甚至也都沒有任何返回值,然后問我這種代碼寫單元測試有任何價值?!完全是浪費(fèi)體力!!
public class ClassA { public void CallMethod() { DoSomethingForYou(); DoSomethingForThem(); DoSomethingForMe(); } } |
其實(shí)提出這個疑問主要有兩個原因?qū)е拢何茨芾斫鈫卧獪y試的目的是什么以及這段代碼的可測試性并不高。(可測試性以及如何提高將在后面章節(jié)介紹)
單元測試的目的是用來確保程式的邏輯如你預(yù)期的方式執(zhí)行,而并不是用來驗(yàn)證是否符合客戶的需求的!通過單元測試來建立一道堅(jiān)實(shí)的保障,防止代碼在日后的修改中不會被破壞掉。
是不是很失望?單元測試并不是用來驗(yàn)證代碼是否符合需求的。
事實(shí)上,單元測試是白盒測試的一種,而且需要開發(fā)人員來完成,最好是誰開發(fā)的代碼就該誰來編寫單元測試代碼,因?yàn)榇a的編寫者最熟悉該段代碼的目的,進(jìn)而編寫出驗(yàn)證該目的是否達(dá)到的單元測試代碼。
單元測試并不能用來代替其他測試手段,不過是實(shí)踐過程中確實(shí)會很有效的幫助開發(fā)人員自查代碼,進(jìn)而發(fā)現(xiàn)一些潛在的BUG。但這只是一個額外的收獲,若不是采用TDD那樣的測試先行開發(fā)方式,那么單元測試的根本目的不是用于也無法檢驗(yàn)當(dāng)前代碼是否存在BUG的!
上面有說到單元測試最好是由開發(fā)人員自己來編寫,用于驗(yàn)證該段代碼是否符合開發(fā)者開發(fā)的預(yù)期要求。這里可能會有個疑問,既然開發(fā)者自己已經(jīng)很清楚自己想要 結(jié)果是什么,直接運(yùn)行一遍代碼實(shí)際跑一次,通過斷點(diǎn)調(diào)試不是就可以很方便的驗(yàn)證了嘛?再通過編寫代碼的形式,甚至比開發(fā)這個功能本身更多的代碼,去驗(yàn)證這 個方法是否符合編寫的目的,不是很傻很笨很累的辦法么?
也許通過一個大家經(jīng)常會碰到的實(shí)際場景能更好的說明:
一個項(xiàng)目開始,項(xiàng)目經(jīng)理把需求拆解為若干個模塊分發(fā)給不同的開發(fā)人員去完成。這樣每個人可能只熟悉自己的那部分代碼。當(dāng)項(xiàng)目某個階段開發(fā)完成并上線后,可能部分開發(fā)人員會離開項(xiàng)目進(jìn)入別的新項(xiàng)目,留下個別人員繼續(xù)維護(hù);或者項(xiàng)目下階段開發(fā)新進(jìn)一大批人員并不熟悉當(dāng)前項(xiàng)目;當(dāng)然最常見的是,在修改BUG階段是無法完全做到誰產(chǎn)生的BUG就安排誰去修改。
那么這時候就會出現(xiàn)一種常見的情況:因?yàn)閷Ξ?dāng)前代碼要滿足的各種目的不熟悉,在修改一個模塊或者BUG的時候把原有正確的功能也影響到了!更要命的是,誰也不知道這個BUG出現(xiàn)了,等待測試人員需要去重新發(fā)現(xiàn)一遍。
于是項(xiàng)目經(jīng)理會發(fā)現(xiàn),每次只要做了代碼修改,無論是重構(gòu)還是功能新增修改,還是修改了BUG,都無法知道當(dāng)前代碼的健壯性,以前編寫的東西是否依然正確可用?
然而如果這個項(xiàng)目在一開始就編寫了單元測試的話,我們可以通過方便的自動化單元測試框架運(yùn)行所有的單元測試,進(jìn)而檢查在此次修改前的所有被單元測試所覆蓋的代碼是否依然正常運(yùn)行(符合以前編寫的單元測試期望,如果驗(yàn)證通過,則認(rèn)為原有代碼未受到影響)
由上我們可以看出,單元測試雖然增加了相當(dāng)大的開發(fā)工作量,但對于一個長期不斷改進(jìn)和維護(hù)的項(xiàng)目而言,單元測試反而是消減整體成本的一個有效手段,它能及時而準(zhǔn)確的發(fā)現(xiàn)在代碼修改之后,原來對代碼要求的功能是否都依然正確滿足。
但這里有個嚴(yán)重缺陷:單元測試無法檢測到某個方法修改后是否對其他方法造成了影響,只能檢測到被修改的方法本身的原有目的是否被影響(這個將在下面的與集成測試的區(qū)別中詳解)
也因此,個人覺得單元測試最適合的場景是基于TDD開發(fā)。若需求發(fā)生改變,修改了一個方法,而多數(shù)情況下也會去修改單元測試代碼,因?yàn)轭A(yù)期也發(fā)生了改變,這個時候又不能檢測到對其他代碼的影響,這時單元測試意義確實(shí)不大。
單元測試與集成測試的區(qū)別是什么?
多數(shù)人其實(shí)一直不能很好的區(qū)分集成測試和單元測試,甚至不少人一直理解的單元測試只能算是集成測試,但其實(shí)兩者的概念是完全不同的。
單元測試測試的對象是每一個獨(dú)立的方法,而且盡可能的隔離方法和其他方法以及其他外界依賴項(xiàng);
集成測試的測試對象是被單元測試檢測后的方法與方法之間的調(diào)用關(guān)系,以及調(diào)用執(zhí)行過程是否符合預(yù)期。
● 針對項(xiàng)目的一部份或全部進(jìn)行測試,可以跨越不同的類別與方法,并可直接存取的外部資源。例如: File I/O, 數(shù)據(jù)庫操作, 網(wǎng)絡(luò)連接, …
● 通常做集成測試都會需要先設(shè)置(Configure)測試所需的環(huán)境,測試完畢后通常要清除測試所產(chǎn)生的殘留資料,以利下次測試或避免影響其他整合測試的結(jié)果。
○ ClassInitialize Attribute
○ ClassCleanup Attribute
○ TestInitialize Attribute
○ TestCleanup Attribute
以上這些屬性常用于集成測試,不能出現(xiàn)在單元測試中。
單元測試的三大基本要素(Trustworthiness/Maintainability/Readability)
1、信任你的測試代碼結(jié)果
1)你是否能信任你的測試結(jié)果?
2)當(dāng)它通過,我們有信心說被測試代碼一定工作。
3)當(dāng)它失敗,它一定證明被測試代碼是錯誤的。
4)如果你不斷的對測試結(jié)果失去信心,那么你也不會繼續(xù)堅(jiān)持撰寫單元測試。有
5)許多人搞不清楚單元測試與集成測試的差別,以致于感覺自己寫的單元測試過于薄弱而不相信測試的結(jié)果。
6)如果你因?yàn)槟承┰驅(qū)е聹y試失敗,直接去改Code或直接去改Test Code都不是好事,你的首要目的是要能找出測試失敗發(fā)生的主因,而非只是看錯誤這件事,這樣你才能信任你的測試程式。
2、測試代碼的可維護(hù)性
1)是否能夠持續(xù)的維護(hù)你的測試程式?
2)如何有效的降低維護(hù)測試程式的成本?
PS:透過一些Testable Design Pattern 可以有效提升可維護(hù)性。例如: Repository Pattern, Service Pattern……
3、測試代碼的可讀性
1)你的測試程式的命名是否易于理解?
2)當(dāng)你測試失敗時是否能從測試失敗的測試方法(TestMethod)明確看出實(shí)際失敗的原因?
3)當(dāng)讀取測試數(shù)據(jù)的人看不懂你的測試,人們就不會執(zhí)行這些測試、也不會去維護(hù)這些測試,久而久之就會越來越惡化。
Test Driven Development & Unit Test
寫在本文最后,其實(shí)我一直覺得單元測試其實(shí)是為了TDD開發(fā)模式而誕生的,在這種開發(fā)模式下使用單元測試完全是非常順暢的:
1、根據(jù)軟件需求文檔拆解軟件功能,并設(shè)計(jì)出功能模塊劃分;
2、根據(jù)需要的功能模塊設(shè)計(jì)出單元測試場景用例,因?yàn)榇藭r可以很清晰的知道能夠提供什么樣的數(shù)據(jù),以及需要達(dá)到什么樣的功能,這對設(shè)計(jì)單元測試用例已經(jīng)完 全足夠了;
3、編寫單元測試代碼,這個時候可以專注于檢驗(yàn)這個方法的是否滿足設(shè)計(jì)的要求,此時甚至實(shí)際的代碼還根本沒開發(fā),而.NET 4.0的Dynamic關(guān)鍵字在這里可以得到充分的發(fā)揮:調(diào)用那些根本都還不存在的方法,卻不會導(dǎo)致編譯無法通過。
4、若在編寫單元測試過程中,可以預(yù)期當(dāng)前這個方法若需要調(diào)用一些其他類或方法的支持,可以通過編寫Mock Object來模擬,同樣也是無需實(shí)現(xiàn)真正的代碼,只需要有基本的代碼框架或者接口即可。
5、在為這個方法編寫好單元測試代碼之后,就可以開始編寫實(shí)際的代碼實(shí)現(xiàn)了,因?yàn)樵谥盀榱藵M足Testability的需要,代碼已經(jīng)是基于依賴倒置模 式的了,無需再擔(dān)心其他需要調(diào)用的類或方法是否已經(jīng)實(shí)現(xiàn)或正確實(shí)現(xiàn)。在編寫好本方法的實(shí)現(xiàn)之后就可以通過運(yùn)行之前的單元測試進(jìn)行驗(yàn)收了。
可以看到,若按照以上這種方式進(jìn)行開發(fā),首先代碼的耦合性是非常低的,其次代碼的質(zhì)量也是很高的,最后還會因?yàn)榇a之間的耦合度低從而降低在開發(fā)過程中, 相互制約進(jìn)度相互影響的可能性。在追查BUG的時候也很有優(yōu)勢:很容易查到BUG是否蔓延。
反之,對一個Legacy System進(jìn)行重構(gòu)使之Testable,再編寫單元測試其實(shí)工作量不小,實(shí)際的收益也不會特別大。
單元測試的基本概念以及價值就基本講完,下篇文章將開始介紹Visual Studio 2010中的單元測試工具與環(huán)境。