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