我曾經(jīng)是一個(gè)不測試主義者,因?yàn)槲铱床坏綔y試的價(jià)值。然后,我試了一段時(shí)間,變得對(duì)它深信不疑。我收集了一些經(jīng)驗(yàn),當(dāng)然還遠(yuǎn)遠(yuǎn)不夠。這篇文章總結(jié)了一些我知道的以及我認(rèn)為我知道的內(nèi)容。
本文的靈感主要來自于《JavaScript Air episode 004》,但這里也有一些原創(chuàng)的內(nèi)容。并且有的來自《TDD: Where did it all go wrong?》。
我不總測試我的代碼,但是當(dāng)我測試的時(shí)候,感覺更好。
—— 我
這是怎么一回事呢?
這,全是因?yàn)榇a:
本文主要關(guān)于單元測試,而不是集成測試或端至端的測試,但在某些方面也可用于其他測試。在實(shí)踐中,測試很少是單一的或非此即彼的,并且這也不是目的。
單元測試成本低廉,因此應(yīng)該成為測試工作中最大的組成部分。編寫和運(yùn)行單元測試都很便宜。因?yàn)樗徊榭创a的特定部分。集成測試則相反,它們包含的代碼更大。
為什么這很重要?
測試可幫助你對(duì)你的代碼放心。對(duì)一個(gè)稍復(fù)雜的問題寫一個(gè)解決方案,然后手動(dòng)測試,你只需要這么做就可以了。有著一定經(jīng)驗(yàn)的你當(dāng)然可以自信地發(fā)布代碼,但是結(jié)果卻往往是拋棄了發(fā)現(xiàn)錯(cuò)誤的第一次機(jī)會(huì)。
測試能讓你體驗(yàn)?zāi)愕拇a中在最極端的條件下是什么樣的。要是傳遞的數(shù)字是負(fù)數(shù),會(huì)怎么樣,在我們總是假定數(shù)值為正的情況下?要是傳遞的根本就不是數(shù)字,會(huì)怎么樣?
每個(gè)人都會(huì)寫出bug,我們都寫過bug。因此,這不是“你能正確地編寫代碼或一次性寫出正確代碼?”的問題,我們都寫過不正確的代碼。這就是我們所做的一切,我們寫的都是不正確的代碼。
——Joe Eames《JavaScript Air 004》
編碼是辛苦,我們都應(yīng)該承認(rèn)這一點(diǎn)。其中的主要原因之一就是,你需要測試代碼,以獲得它能如期表現(xiàn)的信心,不管是什么代碼。
不相信?這里給出了一些附加參數(shù):
測試過的代碼更好
許多人會(huì)告訴你,代碼測試會(huì)導(dǎo)致更好的代碼質(zhì)量。這在使用單元測試,并且至少在測試驅(qū)動(dòng)開發(fā)上有所行動(dòng),即使這些行動(dòng)甚為草率時(shí),尤其如此。原因如下:
如果你的代碼難以測試,那么可能是你代碼沒有寫好。好代碼的定義是什么,這是一個(gè)大問題,但這里要強(qiáng)調(diào)的一句話是一個(gè)很好的經(jīng)驗(yàn)法則,也是大多數(shù)人所贊同的,那就是,好的代碼會(huì)分離關(guān)注點(diǎn)。有經(jīng)驗(yàn)的程序員限制功能體以便于只做一件邏輯上的事情就是這個(gè)原因。
目標(biāo)對(duì)齊
代碼很難測試可能要么是因?yàn)橛刑嗟氖虑橐^續(xù),要么是因?yàn)橛刑嗟囊蕾嚕ɑ騼烧呓杂校?紤]將此視為協(xié)調(diào)利益的一個(gè)問題:在編寫未經(jīng)測試的代碼
時(shí),在速度(或懶惰)和關(guān)注點(diǎn)分離之間存在著利益沖突,并且短期內(nèi)你的代碼是如何被組織的并沒有那么重要。當(dāng)代碼必須測試時(shí),你的目標(biāo)更一致,因?yàn)閷?duì)于寫
得好的代碼,更易于寫測試!
像消費(fèi)者一樣思考
當(dāng)你第一次編寫測試時(shí),你首先要設(shè)計(jì)代碼的API。測試讓你進(jìn)入代碼消費(fèi)模式,在這種模式下,你的代碼需要面對(duì)其他東西的接口。設(shè)計(jì)API,而不那么關(guān)注內(nèi)部運(yùn)作將導(dǎo)致一個(gè)更佳的API設(shè)計(jì),這會(huì)導(dǎo)致模塊的更易消耗,從而促進(jìn)項(xiàng)目代碼的更干凈。
靈感突現(xiàn)
測試會(huì)讓你靈機(jī)一現(xiàn)。通常情況下,因?yàn)樗仁鼓闳ニ伎歼吘壡闆r——零值,10 ^
12,null或undefined。這使得你有機(jī)會(huì)來思考。去反省,以及了解在陌生的環(huán)境下會(huì)發(fā)生什么。僅僅是思考這些的過程,或代碼將面對(duì)的其他情
況,都經(jīng)常會(huì)讓你意識(shí)到代碼可以簡化(以及代碼需要如何保護(hù)自我)。
這些靈感突現(xiàn)的時(shí)刻也可能來自最令人沮喪的情況之一:當(dāng)你的代碼和測試不一致的時(shí)候。你正處于不知道哪個(gè)才正確的兩難境地。如果你碰到這種情況,那么設(shè)計(jì)可能有問題,或者你的前提假設(shè)發(fā)生了變化。把它看成是一個(gè)好兆頭!你的代碼將會(huì)更滿意。
測試可以說明代碼做了什么
沒有人喜歡寫文檔,但當(dāng)你繼承(從一年前的自己,或其他人)或接口的模塊文檔齊全的時(shí)候,絕對(duì)是好的。測試可以成為這樣一種途徑,并且還有一個(gè)額外
的好處是:測試用實(shí)際行動(dòng)證實(shí)代碼。就如同最佳的科學(xué)教師,他們不只是用嘴巴告訴你,氫氣易燃,而是充了一個(gè)氫氣球,讓它升到天花板上,然后在棍子上放一
根點(diǎn)燃的火柴靠近氣球(這是我五年級(jí)時(shí)最難忘的時(shí)刻之一)。
你知道所有bug的共同點(diǎn)嗎?那就是它們通過了所有的測試。所以,當(dāng)你找到一個(gè)bug的時(shí)候,就等于知道測試哪里還需要改進(jìn)。
測試可以使得更容易地加入項(xiàng)目,因?yàn)樗鼈兘沂玖舜a實(shí)際上應(yīng)該做什么。它們告訴你設(shè)計(jì)決策,以及初始的開發(fā)人員心里在想什么。
不要擔(dān)心,去重構(gòu)吧
也曾看到過烏七八糟的代碼,但不敢去清理干凈?我在這種情況下要做的第一件事是創(chuàng)建測試來找出代碼要做什么。測試可以鎖定功能,用一種很好的方式,使得我們能夠?qū)W⒂凇按髵叱保皇菗?dān)心破壞什么東西。
我見過一些糟糕到讓人不知道它們是做什么的代碼片段。同樣的,人人避之唯恐不及,不但要擔(dān)心會(huì)破壞預(yù)期的功能,而且還要擔(dān)心破壞bug。我認(rèn)為基于過去的I/ O的大型測試集是非常值得的投資。
有趣的是,擔(dān)心和快樂的心情是成反比的。總之是一種此消彼長的狀態(tài)。
自信地創(chuàng)造價(jià)值和正確的產(chǎn)品
正確的代碼比不正確的代碼更有價(jià)值。一切幫助你的代碼比以前更正確的東西都值得看一看,就這么簡單。發(fā)布正確的代碼隨著時(shí)間的推移會(huì)構(gòu)建起信任,而信任是一筆寶貴的財(cái)富。
魚與熊掌不可得兼
這里有一個(gè)技巧:不要在試圖解決問題的同時(shí),設(shè)計(jì)一個(gè)很好的解決方案。來自于 Ian Cooper關(guān)于TDD演講中的秘訣是:
編寫紅色測試。
解個(gè)問題,盡快讓它變綠。
設(shè)計(jì)一個(gè)很好的解決方案,重構(gòu)成你為之驕傲的一個(gè)東西。
這里要掌握的一個(gè)重要內(nèi)容是,在你的大腦中要分離關(guān)注點(diǎn)。不要試圖同時(shí)完成步驟2和步驟3。編程的主要限制之一是你的大腦一次能思考多少,并且在你敲代碼時(shí),你需要思考得越少,你寫的代碼越好。
在解決問題時(shí),不要去想代碼實(shí)際上應(yīng)該如何。復(fù)制粘貼代碼,寫低效的循環(huán),重復(fù)內(nèi)容,不論是什么只要能盡快讓測試變綠就去做。然后再考慮如何改進(jìn)。
分離關(guān)注點(diǎn)是首先要測試的原因之一,這種方法有助于實(shí)踐中行為。當(dāng)你不擇手段地想要快速達(dá)成一個(gè)解決方案時(shí),你不必去考慮它看上去怎么樣或者運(yùn)行起來快不快。當(dāng)你進(jìn)行到完善設(shè)計(jì)和改善解決方案的時(shí)候,你就不必?fù)?dān)心解決方法行不通了。
知道測試什么是關(guān)鍵
知道測試什么沒有聽上去得那么容易,并且有很大一部分是由經(jīng)驗(yàn)所決定的。許多測試測試得太多。知道要測試什么涉及到要了解什么重要,什么不重要,而要知道這些并不是一件隨隨便便就能做到的事情。這里有一個(gè)技巧,但:
盡可能采用最高級(jí)別的測試,以便于在實(shí)現(xiàn)上覆蓋范圍和靈活性。
——Brian Lonsdorf,《JavaScript Air 004》
所以,基本上:
不要測試內(nèi)部的東西,這只會(huì)成為你的阻礙。如果你真的覺得你應(yīng)該測試內(nèi)部的東西,那么你最好分離成一個(gè)新的模塊,使之成為外部的東西。
不要測試過于指定,或處理它們不必和不應(yīng)該知道的東西。
不要只是為了獲得100%的覆蓋率而去寫測試。如果有人告訴你應(yīng)該保持100%的覆蓋率,那么不要廢話,揍他。
請(qǐng)記住,測試應(yīng)該從模塊外部的角度開始由外到內(nèi)。需要注意的是完全覆蓋的測試還是有可能的,即代碼的所有分支應(yīng)該都可以實(shí)現(xiàn)。如果沒有,那么它們基本上是死碼,不是嗎?除非你需要更好地理解它們是如何工作的,否則就不要測試內(nèi)部的東西。
想想當(dāng)一段時(shí)間以后,代碼重構(gòu)的時(shí)候,會(huì)發(fā)生什么。實(shí)現(xiàn)應(yīng)該允許在測試不失敗的情況下被更改。為什么?因?yàn)槿绻麑淼某绦騿T需要改測試的話,那么基本上是重寫,而不是重構(gòu)。并且重寫并不安全。對(duì)于重構(gòu)內(nèi)部應(yīng)該沒有新的測試。
在測試時(shí)要?jiǎng)?wù)實(shí)。測試是項(xiàng)目以及創(chuàng)造價(jià)值的一部分,什么都拿來測試沒有任何意義,就像實(shí)現(xiàn)所有按鈕沒有意義一樣。記住文檔方面。如果測試涉及許多實(shí)施細(xì)節(jié),那么我們就會(huì)失去模塊的重點(diǎn)。我們就會(huì)失去文檔的價(jià)值。
至于文檔,測試你的領(lǐng)域假設(shè)。這些都是你工作的問題域的代碼解釋,這些問題域往往是一些程序員不擅長的地方。用代碼的形式文檔化這些假設(shè)解決了兩個(gè)問題:自我文檔化假設(shè),并證明它們能夠如解釋那樣有效工作。
當(dāng)你發(fā)現(xiàn)bug的時(shí)候,編寫測試。不要只是修復(fù)它。去寫測試,確保它既是紅的,又對(duì)齊bug所沒有意識(shí)到的期望。修復(fù)bug,使其呈現(xiàn)綠色。保存。
代碼覆蓋作為一個(gè)具體的數(shù)字被高估了,但作為一種工具它還是很有用的。不要為了覆蓋范圍而力求覆蓋。請(qǐng)記住,覆蓋范圍只能告訴你測試在代碼行運(yùn)行什
么,而不會(huì)告訴你測試將運(yùn)行什么組合。不過,這可以成為事情是否朝著正確方向前進(jìn)的一個(gè)很好的風(fēng)向標(biāo)。如果重構(gòu)導(dǎo)致更糟的代碼覆蓋范圍,那么就應(yīng)該響起警
鈴,尤其是如果它是重構(gòu)的話。不要只是為了增加覆蓋數(shù)值就讓自己去編寫測試。經(jīng)過充分測試和編寫良好的代碼的覆蓋數(shù)值更大。
編寫測試的觸發(fā)器是當(dāng)你的代碼片段有新的行為的時(shí)候。測試應(yīng)該盯牢這種行為,但不要矯枉過正。
測試庫可能比測試終端應(yīng)用程序更容易,更為重要。畢竟,庫會(huì)被多個(gè)應(yīng)用程序使用。
如何編寫特別棒的測試
知道如何寫出好的測試是關(guān)鍵,因?yàn)楹苋菀讓懙貌缓谩J聦?shí)是,和其他所有一切一樣,它需要實(shí)踐。不過,這里有一些小貼士。
好的測試往往是簡單的。它不會(huì)嘗試一氣呵成面面俱到。它的名字反映了它要的目的,并且名稱應(yīng)該精簡成一句話。例如,名稱不應(yīng)該是“it works”,而是“it returns 0 for negative values”。
確保測試不要過于指定。過于指定的測試涉及到太多內(nèi)部東西,并且不允許重構(gòu)。
單元測試運(yùn)行代碼時(shí)會(huì)隔離其他測試,不一定是其他代碼的測試。它將代碼帶出它的上下文,并創(chuàng)建其中一個(gè)方面的人工上下文,以便于進(jìn)行調(diào)查。然而,這并不意味著單元測試必須得在隔離其他所有代碼的情況下運(yùn)行,盡管這通常被認(rèn)為是“純單元測試”。所有一切都沒有必要mock和stub,因?yàn)橹粫?huì)導(dǎo)致更復(fù)雜的設(shè)置,更低的覆蓋率和更加脆弱的測試。
在有意義的地方使用mock和stub。你不想對(duì)一個(gè)真正的HTTP API進(jìn)行測試,那就stub。如果你正在測試的東西是你自己對(duì)該對(duì)象的調(diào)用,或你想要自己的代碼歷經(jīng)某個(gè)路徑,那么使用使用mock和stub。
測試讀起來應(yīng)該像一個(gè)小故事,遵循AAA體系: Arrange、Act、Assert。設(shè)置東西,做出聲明,并且斷言聲明做了它應(yīng)該做的。“小故事”方面要重視小的方面。“3A”中沒有一個(gè)應(yīng)該超過3行代碼以上。在階段之間留一些空間會(huì)更好。應(yīng)該沒有任何分支和循環(huán),你在斷言時(shí)應(yīng)該只涉及一個(gè)邏輯內(nèi)容。 (如果一個(gè)斷言語句就能表達(dá)自然是好,但有時(shí)你需要更多,那也沒關(guān)系。)永遠(yuǎn)不要在測試的兩個(gè)不同的地方斷言,因?yàn)檫@會(huì)導(dǎo)致你實(shí)際測試的混亂。
測試應(yīng)該只需要一些領(lǐng)域知識(shí)就可讀。如果不深入模塊的內(nèi)部運(yùn)作就很難解釋的話,那么要么你最好多花一些時(shí)間在測試上,那么徹底棄之不顧。
一般情況下,不要測試依賴。對(duì)于某些項(xiàng)目,對(duì)一些代碼所做的假設(shè)做一些簡單的測試,可能是有意義的,但要謹(jǐn)慎和小心。測試庫是庫作者的工作。相反,要依靠更新日志進(jìn)行升級(jí),以及依賴于測試集成而不是庫(不用mock一切的一個(gè)原因)。
編寫不需要很長時(shí)間運(yùn)行的低成本測試,因?yàn)橐獣r(shí)常運(yùn)行這些測試。如果你可以傳遞--watch參數(shù)到你的測試運(yùn)行中,并且在每次有文件改變時(shí)運(yùn)行它,那么這是一件好事。
最后但并非最不重要的一點(diǎn)是,使用你喜歡的測試框架。如果JavaScript是你的菜,那么我會(huì)推薦AVA,因?yàn)樗逦唵危覜]有復(fù)雜的配置。不管你選擇什么,確保測試框架能和你一起工作,并幫助你編寫測試更高效,更快捷。正如編碼一樣,如果你覺得不好玩,那么可能有什么地方出錯(cuò)了。