1、自傲的編碼
有一次——或許就是上個禮拜二——有兩個開發者:Pat 和Dale。他們面臨著不異的最后一日,而這一天也越來越近了。Pat 天天都在焦心地編寫代碼,寫完一個類又寫一個類,寫完一個函數又接著寫另一個函數,還經常不得不停下來做一些調整,使得代碼能夠經由過程編譯。
Pat 一向連結著這種工作體例,直到最后一日的前一天。而這時已經是演示所有代碼的時候了。Pat 運行了最上層的軌范,可是一點輸出也沒有,什么都沒有。這時只好用調試器來單步跟蹤了。“Hmm,決不成能是這樣的”,Pat 想,“此時這個變量絕對不是0 啊”。于是,Pat 只能回過頭來看代碼,考慮著跟蹤一下這個難以琢磨的程序的挪用流程。
時刻已經越來越晚了,Pat 找到且更正了這個bug;但在這個過程中,Pat 又找到了其他好幾個bug;如斯幾回事后,bug 仍是存在。而程序輸出何處,仍然沒有結果。這時,Pat 已經筋疲力盡了,完全搞不清為什么會這樣,認為這種(沒有輸出的)行為是毫無事理的。
而于此同時,Dale 并沒像Pat 那么快地寫代碼。Dale 在寫一個函數的時候,會附帶寫一個簡短的測試程序來測試這個函數。這并沒有什么非凡的處所,只是添加了一個簡單的測試,來判定函數的功能是否和程序員期望的一致。顯然,考慮如何寫,然后把測試寫出來,是需要占用一定時間的;可是Dale 在未對剛寫的函數做出確認之前,是不會接著寫新代碼的。也就是說,只有等到已知函數都獲得確認之后,Dale 才會繼續編寫下一個函數,然后挪用前面的函數等等。
在整個過程中,Dale 幾乎不使用調試器;而且對Pat 的模樣也有些思疑不解:只見他頭埋在兩手之間,嘀咕著各類難聽的話語,詛咒著計較機,充血的眼球同時盯著好幾個底時景口。
最后一日終于到了,Pat 未能完成使命。而Dale 的代碼被集成到整個系統中,而且能夠很好地運行。之后,在Dale 的模塊中,呈現了一個小問題;可是Dale 很快就發現了問題地址,在幾分鐘之內就解決了問題。
此刻,是該總結一下這個小故事的時候了:Dale 和Pat 的年數相當,編碼能力相當,智力也差不多。唯一的區別就是Dale 很是相信單元測試;對于每個新寫的函數,在其他代碼使用這個函數并對它形成依靠之前,都要先做單元測試。
而Pat 則沒有這么做,他老是“知道”代碼的行為應該和所期望的完全一樣,而且等到所有代碼都差不多寫完的時候,才想起來運行一下代碼。然而到了這個時辰,要想定位bug,或者,甚至是確定哪些代碼的行為是正確的,哪些代碼的行為是錯誤的,都為時已晚了。
2、什么是單元測試
單元測試是開發者編寫的一小段代碼,用于磨練被測代碼的一個很小的、很明晰的功能是否正確。凡是而言,一個單元測試是用于判定某個特定前提(或者場景)下某個特定函數的行為。例如,你可能把一個很年夜的值放入一個有序list 中去,然后確認該值呈現在此刻list 的尾部。或者,你可能會年夜字符串中刪除匹配某種模式的字符,然后確認字符串確實不再包含這些字符了。
執行單元測試,是為了證實某段代碼的行為確實和開發者所期望的一致。
對于客戶或最終使用者而言,這種測試需要嗎,它與驗收測試有關嗎?這個問題仍然很難回覆。事實上,我們在此并不關心服個產物確認、驗證和正確性等等;甚至此時,我們都不去關心性能方面的問題。我們所要做的一切就是要證實代碼的行為和我們的期望一致。所以,我們所要測試的是規模很小的、很是獨立的功能片段。經由過程對所有零丁部門的行為成立起抉擇信念,確信它們都和我們的期望一致;然后,我們才能起頭組裝和測試整個系統。
事實下場,若是我們對手上正在寫的代碼的行為是否和我們的期望一致都沒把握,那么其他形式的測試也都只能是華侈時刻而已。在單元測試之后,你還需要其他形式的測試,有可能是更正規的測試,那一切就都要看情形的需要來抉擇了。總之,做測試如同做善事,老是要巨匠(代碼最根基的正確性)起頭。
3、為什么要使用單元測試
單元測試不單會使你的工作完成得更輕松,而且會令你的設計變得更好,甚至削減你花在調試上的時間。
在我們之前的小故事中,Pat 因為假設底層的代碼是正確無誤的而卷入麻煩之中,先是高層代碼中使用了底層代碼;然后這些高層代碼又被更高層的代碼所使用,如斯往來來往。在對這些代碼的行為沒有任何抉擇信念的前提下,Pat 等于是在假設用豎立卡片堆砌了一間房子——只要將下面卡片輕輕移動,整間房子就會轟然傾塌。
當根基的底層代碼不再靠得住時,那么必需的改動就無法只局限在底層。雖然你可以批改底層的問題,可是這些對換層代碼的改削必然會影響到高層代碼,于是高層代碼也連帶地需要修改;以此遞推,就很可能會動到更高層的代碼。于是,一個對換層代碼的批改,可能會導致對幾乎所有代碼的陸續串改動,如此而使改動越來越多,也越來越復雜。于是,整間由卡片堆成的房子就由此傾塌,從而使整個項目也以失蹤失敗了卻。
Pat 老是說:“這怎么可能呢?”或者“我其實想不明白為什么會這樣”。你發現自己有時候也會有這種想法。那么凡是你對自己的代碼還缺乏足夠抉擇信念的默示——你并不能確認哪些是工作正常的而哪些不是。
為了獲得Dale 所具有的那種對代碼的抉擇信念,你需要“詢問”代碼事實做了什么,并搜檢所發生的結過是否確實和你所期望的一致。
這個簡單的設法描述了單元測試的焦點內在:這個簡單的手藝就是為了令代碼變得加倍完美。
4、我需要做什么
其實惹人的單元測試是很簡單的,因為它自己就布滿了樂趣。然而在項目交付的時候,我們給客戶和最終用戶的仍然是產物代碼,而不包含單元測試的代碼;所有,我們必需對單元測試的目的有個充實的熟悉。首先也是最主要的,使用單元測試是為了使你的工作——以及你隊友的工作——完成得加倍輕松。
● 它的行為和我的期望一致嗎?
最根柢的,你需要回覆下面這個問題:“這段代碼達到我的目的了嗎?”也許代碼所做的是錯誤的工作,但那是另外的問題了。你要的是代碼向你證實它所做的就是你所期望的。
● 它的行為一向和我的期望一致嗎?
很多開發者說他們只編寫一個測試。也就是讓所有代碼從頭至尾跑一次,只測試代碼的一條正確執行路徑,只要這樣走一遍下來沒有問題,測試也就算是完成了。
可是,現實當然不會這么事事順心,工作也不老是那么順利:代碼會拋出異常,硬盤會沒有殘剩空間,收集會失蹤線,緩沖區會溢出等——而我們寫的代碼也會呈現bug。這就是軟件開發的“工程”部門。就“工程”而言,土木匠工程師在設計一座橋梁的時候,必需考慮橋梁的負載、強風的影響、地震、洪水等等。電子工程師要考慮頻率漂移、電壓尖峰、噪音,甚至這些同時呈現時所帶來的問題。
你不能這樣來測試一座橋梁:在風和日麗的某一天,僅讓一輛車順遂地開過這座橋。顯然,這種測試對于橋梁測試來說是遠遠不夠的。相似地,在測試某段代碼的行為是否和你的期望一致時,你需要確認:在任何情形下,這段代碼是否都和你的期望一致;譬如在參數很可疑、硬盤沒有殘剩空間、收集失蹤線等的時候。
● 我可以依靠單元測試嗎?
不能依靠的代碼是沒有多大用處的。但更糟糕的是,那些你自認為可以相信的代碼(可是結果證實這些代碼是有bug 的)有時候也會讓你花很多時間在跟蹤和調試上。顯然,幾乎沒有項目可以許可你在這上面花費太多的時間,是以無論如何,你都要避免這種“前進一步,萎縮后退兩步”的開發體例。也就是說,要閃開開發過程連結不變的軌范前進。
沒人能夠寫出十全十美的代碼;可是這并沒有關系——只要你知道問題的地址就足夠了。很多類型軟件項目的失敗,諸如只能把壞了的太空船擱淺在遙遠的行星,或者在翱翔的途中就爆炸了,都能經由過程確認的限制來避免。例如,Arianne 5 號火箭軟件重用了來自于之前一個火箭項目的一個程序庫,而這個程序庫并不能措置新火箭的翱翔高度(比原本火箭要高),從而在起飛40 秒之后就發生了爆炸,導致5 億美元的損失蹤。
顯然,我們但愿能夠依靠于所編寫的代碼,而且清楚地知道這些代碼的功能和約束。
例如,假設你寫了一個反轉數值序列的體例。在測試的過程中,你也許會傳一個空序列給這個程序——但導致了程序解體。現實上,軌范并沒有要求該軌范必需能夠領受一個空序列,是以你可以只在體例的注釋中聲名這個約束:如不美觀傳遞一個空序列給這個體例,那么這個體例將會拋出一個異常。此刻你馬上就知道了該代碼的約束,年夜而也就不需要用其他很麻煩的體例來解決這個問題(因為在某些地址要解決這個問題并未便利,好比在高空年夜氣層中)。
● 單元測試聲名我的意圖了嗎?
對于單元測試而言,一個最讓人歡快的意外收成就是它能夠輔佐你充實理解代碼的用法。簡單而言,單元測試就像是能執行的文檔,了然在你用各類前提挪用代碼時,你所能期望這段代碼完成的功能。
項目成員能夠經由過程查看單元測試來找到如何使用你所寫代碼的例子。如果他偶然發現了一個你沒有考慮到的測試用例,那么他也可以很快地知道這個事實:你的代碼可能并不支持這個用例。
顯然,在正確性方面,可執行的文檔有它的優勢。與通俗的文檔分歧的是,單元測試不會呈現與代碼紛歧導致的情形(當然,除非程序選擇不運行這些測試)。
5、如何進行單元測試
單元測試原本就是一項簡單易學的手藝;可是如果能夠遵循一些指導性原則(guideline)和根基規范,那么進修將會變得加倍輕易和有用。
首先要考慮的是在編寫這些測試用例之前,如何測試那些可疑的用例。有了這樣一個概略的想法之后,你將可以在編寫實現代碼的時候,或者之前,編寫測試代碼。
下一步,你需要運行測試用例,或者同時運行系統的所有其他測試,甚至運行整個系統的測試,前提是這些測試運行起來相對斗勁快。在此,我們要確保所有的測試都能夠經由過程,而不只是新寫的測試能夠經由過程;這一點長短常主要的。也就是說,在保證不惹人直接bug 的同時,你也要保證不會給其他的測試帶來破損。
在這個測試過程中,我們需要確認這個測試事實是經由過程了還是失敗了——但這并不意味著你或者其他晦氣的人需要查看每個輸出,然后才抉擇這些代碼是正確的還是錯誤的。
在此,你慢慢地就會養成一個習慣:只要進行一次單元測試查看一下測試結果,就可以馬上知道所有代碼是否都是正確的,或者哪些代碼是有問題的。關于這個問題,我們將留在討論如何使用單元測試框架時來具體討論。