接下來(lái)談?wù)剢卧獪y(cè)試如何堅(jiān)持下來(lái)的問題
相信大家或因?yàn)樯鐓^(qū)影響匈庭、或因?yàn)樯霞?jí)領(lǐng)導(dǎo)的要求夫凸、抑或純粹的想挑戰(zhàn)自身的編碼水平,也嘗試寫過單元測(cè)試阱持,或許都已經(jīng)上了Jenkins夭拌、TravisCI等集成工具。
想必最初看到單元測(cè)試一路綠燈的時(shí)候衷咽,自己的內(nèi)心一定是愉悅的鸽扁。不錯(cuò)喲今天又向更高的軟件質(zhì)量邁進(jìn)了堅(jiān)實(shí)的一步!然而隨著開發(fā)的持續(xù)镶骗,單元測(cè)試逐漸變得偶爾失敗桶现,再到持續(xù)的失敗,等大家閑下來(lái)想補(bǔ)下單元測(cè)試的時(shí)候鼎姊,發(fā)現(xiàn)之前寫的測(cè)試已經(jīng)祖國(guó)江山一片紅巩那,完全不能使用了。想到又要補(bǔ)充大量的測(cè)試此蜈,以及去讀那些之前晦澀的測(cè)試代碼在再三的衡量工作量之后,覺得代價(jià)太大無(wú)法承擔(dān)噪生,于是你不得不承認(rèn)裆赵,寫單元測(cè)試的又一次努力最終以失敗告終。
這種事情是不是很熟悉跺嗽?甚至在我司也經(jīng)歷了數(shù)次反復(fù)战授,摸爬滾打之后最終才調(diào)整過來(lái)。那么這些反復(fù)的出現(xiàn)到底是哪里出了問題桨嫁?
我總結(jié)了以下幾條
- 做單元測(cè)試沒有明確的目標(biāo)感
- 我知道單元測(cè)試失敗了植兰,但是測(cè)試環(huán)境過了,業(yè)務(wù)能過
- 不知道寫的測(cè)試失敗了
做單元測(cè)試沒有明確的目標(biāo)感
如果我們做一件事情沒有明確的目標(biāo)感璃吧,或者對(duì)做這個(gè)事的意義只有模糊的感受楣导,那這個(gè)事情確實(shí)會(huì)變得十分難堅(jiān)持下去。
當(dāng)然會(huì)有朋友指出畜挨,我寫單元測(cè)試當(dāng)然有目標(biāo)啊筒繁,是為了 提高軟件質(zhì)量 啊噩凹,這么重要的目標(biāo)怎么能說(shuō)沒有呢。 但是毡咏,這真的是個(gè)目標(biāo)么驮宴? 想想上學(xué)時(shí)代的晨跑、或者在背單詞的時(shí)候呕缭,不都也有“我這是為了健康”堵泽、或者“我這是為了提高英語(yǔ)”,為什么沒有最后堅(jiān)持下來(lái)恢总?
因?yàn)檫@種目標(biāo)過于遠(yuǎn)大迎罗,以至于你感覺不到它的變化。除非有一個(gè)強(qiáng)大的內(nèi)心离熏,否則堅(jiān)持這件事情真的會(huì)變得很難佳谦。
那么,就讓我們把目標(biāo)變得具體起來(lái)滋戳。
以前看《把時(shí)間當(dāng)做朋友》的時(shí)候钻蔑,說(shuō)了一個(gè)關(guān)于記單詞的有趣故事
因?yàn)椋还惨愣?0,000個(gè)單詞奸鸯,而因此可能獲得的獎(jiǎng)學(xué)金是每年40,000美元左右——并且連續(xù)四年沒有失業(yè)可能(后來(lái)的事實(shí)是咪笑,他直到五年之后才獲得了博士學(xué)位)。當(dāng)時(shí)的美元兌換人民幣的匯率差不多是8:1娄涩,所以窗怒,大約應(yīng)該相當(dāng)于320,000元人民幣。而如果一年的稅后收入是320,000元人民幣的話蓄拣,那么稅前就要賺取差不多400,000元人民幣扬虚。那么,每個(gè)單詞應(yīng)該大約值20元人民幣——這還只不過是這算了一年的收入而已球恤。
所以辜昵,他終于明白背單詞是非常快樂的咽斧。他每天都強(qiáng)迫自己背下200個(gè)單詞堪置。而到了晚上驗(yàn)收效果的時(shí)候,每在確定記住了的單詞前面畫上一個(gè)勾的時(shí)候张惹,他就要想象一下剛剛數(shù)過一張20元人民幣的鈔票舀锨。每天睡覺的時(shí)候總感覺心滿意足,因?yàn)榻裉煊仲嵙?000塊宛逗!
如果你是 Team leader
坎匿,那么問題就變得十分簡(jiǎn)單了:給出一條線即可:提交的代碼測(cè)試覆蓋率達(dá)到70%以上,且能跑過的代碼才是好代碼。最開始可以就是models 層的單元測(cè)試碑诉。整個(gè)團(tuán)隊(duì)會(huì)因?yàn)榛羯P?yīng)從而做出相應(yīng)的轉(zhuǎn)變彪腔。制定這個(gè)的規(guī)定有一個(gè)技巧:不要人工的去進(jìn)行評(píng)判
,可以在git server 的hook 加一個(gè)腳本來(lái)進(jìn)行認(rèn)證进栽。因有了人工的判斷德挣,就會(huì)有特例,一旦開了特例快毛,馬上就會(huì)有下一個(gè)特例格嗅,具體參見破窗理論。
當(dāng)然做出這一點(diǎn)的前提有兩個(gè):首先唠帝,你自己得信單元測(cè)試屯掖,其次,你得是領(lǐng)導(dǎo)襟衰,頂?shù)米毫?/code>贴铜。
如果你不是領(lǐng)導(dǎo),那我們拿什么來(lái)說(shuō)服自己寫測(cè)試呢瀑晒?
當(dāng)年我最初寫單元測(cè)試的目的非常簡(jiǎn)單:避免我犯下的低級(jí)錯(cuò)誤進(jìn)入到代碼倉(cāng)庫(kù)绍坝,被別人看到了我丟不起這人。因?yàn)樽约河肐DE 調(diào)試的時(shí)間與單元測(cè)試的時(shí)間實(shí)際上差不多苔悦,更重要的是轩褐,我可以向同事吹噓:我信仰自動(dòng)化,我測(cè)試覆蓋率能達(dá)到XX
玖详。
我知道單元測(cè)試失敗了把介,但是我測(cè)過了,業(yè)務(wù)能過
隨著開發(fā)與測(cè)試的持續(xù)進(jìn)行蟋座,幾乎所有人都發(fā)現(xiàn)了一個(gè)很惱火的事情 “原來(lái)寫的單元測(cè)試因?yàn)榇a的變化導(dǎo)致運(yùn)行失敗了,但是我測(cè)過了拗踢,業(yè)務(wù)上沒問題”
。
首先有一個(gè)很重要的原則:
一旦單元測(cè)試掛了向臀,團(tuán)隊(duì)?wèi)?yīng)該首先解決這個(gè)問題
注意秒拔,這里是“單元測(cè)試”,而不是"UI級(jí)別的功能測(cè)試"飒硅,后者以后有機(jī)會(huì)再聊。
首先要說(shuō)作谚,單元測(cè)試為什么會(huì)掛三娩,我總結(jié)的原因如下:
- 由于語(yǔ)法錯(cuò)誤導(dǎo)致的掛
- 配置原因?qū)е碌膾?/li>
- 因?yàn)榇a結(jié)構(gòu)發(fā)生改變,從而導(dǎo)致的之前寫的單元測(cè)試失敗
- 部分測(cè)試有一定概率的失敗妹懒,即常說(shuō)的測(cè)試出現(xiàn)了“假摔”
語(yǔ)法一旦出錯(cuò)雀监,立即去修,沒有什么好說(shuō)的,因?yàn)檎Z(yǔ)法出錯(cuò)必然導(dǎo)致了部分或全部業(yè)務(wù)跑不起來(lái)会前。
配置原因多半是因?yàn)闇y(cè)試依賴的第三方組件并沒有啟動(dòng)起來(lái)好乐,這也是大多數(shù)人不推薦數(shù)據(jù)庫(kù)的原因,我對(duì)此點(diǎn)的態(tài)度中立瓦宜,我覺得可以依賴部分外部環(huán)境的蔚万,只要速度足夠快,沒什么不好的临庇,畢竟在真正部署上線的時(shí)候反璃,你也真是依賴它,如果在平時(shí)測(cè)試的時(shí)候就能熟練處理這些問題假夺,到真正上線的時(shí)候也不會(huì)忙的手忙腳亂的淮蜈。
因?yàn)榇a結(jié)構(gòu)發(fā)生改變,從而導(dǎo)致了測(cè)試的失敗已卷,這種情況通常有兩種情況梧田,第一種是我們喜聞樂見的:我們發(fā)現(xiàn)了過去的一個(gè)舊方法因?yàn)槠渌a的改變而發(fā)生了改變,導(dǎo)致了最后的處理不符合我們?cè)仍O(shè)計(jì)的這個(gè)函數(shù)的目的侧蘸。這正是將測(cè)試自動(dòng)化的好處裁眯,我們沒有足夠的時(shí)間精力去測(cè)我們沒有修改過的函數(shù)的正確性,但是機(jī)器可以闺魏。 這種情況沒有什么好說(shuō)的修bug 就好了未状。第二種情況是:我非常確定對(duì)代碼的修改會(huì)影響到其他舊有函數(shù)的功能,且這個(gè)變化是我期待的正確的業(yè)務(wù)處理變更析桥,換句話說(shuō):“測(cè)試測(cè)的是老舊的業(yè)務(wù)方式司草,已經(jīng)不是最新的期待的函數(shù)的功能了”。這種情況對(duì)應(yīng)的就是修改單元測(cè)試本身泡仗。然而這里就有個(gè)問題了埋虹,我發(fā)現(xiàn)隨便的修改都會(huì)導(dǎo)致單元測(cè)試報(bào)錯(cuò),修改單元測(cè)試的成本變得非常的高娩怎,但是需求卻是滿足的搔课。
這就涉及到了單元測(cè)試在編寫的時(shí)候的一些問題了,這是我在寫單元測(cè)試編寫的時(shí)候的實(shí)踐:
- 不要重復(fù)檢測(cè)已經(jīng)檢測(cè)過的邏輯截亦。
盡量避免在A 函數(shù)的單元測(cè)試中去測(cè)試B函數(shù)的功能爬泥。這個(gè)含義非常重要,在初期我們寫單元測(cè)試的時(shí)候通常會(huì)很隨意崩瓤,往往覺得這個(gè)地方有點(diǎn)猶豫袍啡,我就需要assert 一下,但這么做的缺點(diǎn)就變成了本來(lái)A函數(shù)的Test 由A來(lái)保證却桶。舉個(gè)例子:
it "could build adult correctly" do
person = Person.init_adult
assert_equal true, person.age >= 18
end
it "adult could buy beer" do
person = Person.init_adult
assert_equal true, person.age >= 18 # 又一次檢測(cè)了init_adult的正確與否
assert_equal true, person.could_buy_beer
end
上面的代碼當(dāng)init_adult 發(fā)生改變境输,age 由18 提升到了21 的時(shí)候蔗牡,兩個(gè)測(cè)試會(huì)同時(shí)掛,但是這里的buy_beer 函數(shù)本身是沒有發(fā)生任何邏輯上的錯(cuò)誤的嗅剖,假設(shè)更多的測(cè)試用例都增加了person = Person.init_adult; assert person.age > 18
的話辩越,有可能更多的測(cè)試都會(huì)失敗,而你從眾多的測(cè)試失敗中想找到真正的測(cè)試失敗的adult_init
的難度也會(huì)變得更大信粮。產(chǎn)品又催的急黔攒,很可能你就不會(huì)修復(fù)這個(gè)測(cè)試,只手動(dòng)測(cè)試一下產(chǎn)品的邏輯蒋院,沒問題亏钩,就上線了。下次你再跑測(cè)試的時(shí)候欺旧,又發(fā)現(xiàn)掛一片姑丑,感覺修改無(wú)力,遂放棄辞友,轟轟烈烈的單元測(cè)試?yán)硐刖痛耸×恕?/p>
- 謹(jǐn)慎對(duì)待私有函數(shù)的測(cè)
對(duì)私有函數(shù)的測(cè)試我保持一個(gè)謹(jǐn)慎的態(tài)度栅哀。其他語(yǔ)言測(cè)試私有方法會(huì)比較麻煩,但是這個(gè)麻煩在ruby這門語(yǔ)言上是不存在了:僅僅需要調(diào)用call
就可以輕松的使用私有方法称龙。但是我們是否要真正的測(cè)試私有方法呢留拾?
私有方法一定會(huì)被公開的方法調(diào)用,否則私有方法就沒有存在的意義鲫尊。如果私有方法出現(xiàn)了問題痴柔,一定會(huì)反應(yīng)到最終調(diào)用的共有方法身上。那么如果我對(duì)私有方法本身做一些優(yōu)化或者修改疫向,只要不影響到公開的方法的行為咳蔚,為什么要讓測(cè)試失敗呢?同時(shí)我也認(rèn)為私有方法是不穩(wěn)定的搔驼,因?yàn)闆]有對(duì)外公開暴露谈火,所以這個(gè)方法隨時(shí)隨地都有可能被人修改,甚至因?yàn)槠渌膬?yōu)化而被刪除舌涨,這樣就會(huì)導(dǎo)致之前對(duì)私有方法的測(cè)試莫名的掛掉糯耍。
當(dāng)然有些人覺得只要一個(gè)方法稍微復(fù)雜一點(diǎn),就有必要對(duì)其進(jìn)行測(cè)試囊嘉,這個(gè)概念我也比較贊同温技,所以我中庸的贊同這樣一個(gè)方式處理
當(dāng)我不確定我新編寫的私有方法的正確性的時(shí)候,我對(duì)其寫上單元測(cè)試扭粱。同時(shí)我也會(huì)在測(cè)試旁邊備注到:如果將來(lái)你跑這個(gè)單元測(cè)試掛了的時(shí)候舵鳞,請(qǐng)刪除掉這個(gè)測(cè)試。這樣就避免了別人在測(cè)試的時(shí)候看到這個(gè)莫名的私有方法測(cè)試失敗后又不知道如何處理的尷尬焊刹。
關(guān)于假摔,我總結(jié)了如下的可能
- 依賴有網(wǎng)絡(luò),但網(wǎng)絡(luò)不穩(wěn)定
- 異步導(dǎo)致失敗
- 加載順序?qū)е碌募偎?/li>
我的想法是,在單元測(cè)試的時(shí)候虐块,就不要有 “依賴網(wǎng)絡(luò)” 這個(gè)情況俩滥。如果真的有依賴網(wǎng)絡(luò),請(qǐng)打stub贺奠,或者請(qǐng)從代碼編寫的時(shí)候遵循依賴注入
的原則霜旧。
異步也是單元測(cè)試?yán)锏募芍M,我在上篇文章中已經(jīng)介紹過了如何避免儡率,這里就不再?gòu)?fù)述了挂据。
關(guān)于單元測(cè)試加載順序?qū)е碌募偎ぃ话愦嬖谟诮忉屝驼Z(yǔ)言里儿普,比如ruby崎逃。這主要是因?yàn)檎Z(yǔ)言本身的性質(zhì)決定的: ruby可以在任意時(shí)刻改變 class,但是運(yùn)行單元測(cè)試的時(shí)候是在運(yùn)行前加載所有的class眉孩,每個(gè)case 跑完的時(shí)候并不做重新加載个绍。如果你在某個(gè)case中修改了某個(gè)class 的方法,其他的測(cè)試也會(huì)被影響浪汪。當(dāng)你運(yùn)氣好的時(shí)候巴柿,這個(gè)case 在其他受影響的case跑完后運(yùn)行,測(cè)試ok死遭,但是運(yùn)氣不好的時(shí)候广恢,后面的測(cè)試對(duì)于你來(lái)講,就變成了“莫名其妙的掛掉”呀潭。解決他的方式很簡(jiǎn)單粗暴钉迷,改了之后記得改回來(lái)就好了。合理的使用alias_method
方法會(huì)很好的解決這個(gè)問題蜗侈。解決這類問題的核心的原則就是讓單元測(cè)試不依賴對(duì)case執(zhí)行的順序篷牌。
總而言之,不要讓測(cè)試掛了后大家仍然無(wú)動(dòng)于衷我們?nèi)藢?duì)于少數(shù)能夠快速處理的問題的時(shí)候踏幻,往往是愿意解決的枷颊,但是當(dāng)問題變得巨大無(wú)比的時(shí)候,行動(dòng)就變得沒那么容易了该面。正所謂防微杜漸就是這個(gè)道理夭苗。
最后說(shuō)一個(gè)情況,簡(jiǎn)單卻又重要:“不知道寫的測(cè)試失敗了”
不知道寫的測(cè)試失敗了
為什么寫的單元測(cè)試失敗了我們不知道隔缀?因?yàn)槲覀儧]有運(yùn)行這些單元測(cè)試题造。別笑,這就是事實(shí)猾瘸。
跑測(cè)試界赔,只需要簡(jiǎn)單的一個(gè)命令行就行了丢习,但是我們真的隨時(shí)隨地都運(yùn)行了這條命令么?并沒有淮悼。原因主要是:
- 跑完單元測(cè)試對(duì)于過于漫長(zhǎng)了
- 忘記要運(yùn)行測(cè)試了
單元測(cè)試應(yīng)該合理的控制時(shí)間咐低,比較多的推薦是5mins, 最長(zhǎng)不超過10 mins。我仍然對(duì)此時(shí)間表示巨大的懷疑袜腥,每次運(yùn)行單元測(cè)試见擦,盯著屏幕上的綠點(diǎn),都有一種經(jīng)歷了一次奇點(diǎn)爆炸到宇宙湮滅的感覺羹令。我能忍受的速度最多是10s鲤屡,于是只運(yùn)行正在編寫的方法的單元測(cè)試就變成了合理的策略。但是這樣的話福侈,其他的測(cè)試似乎又沒有運(yùn)行到酒来?答案很簡(jiǎn)單,交給持續(xù)集成工具(CI)去做就好了癌刽。
市面上有大把的持續(xù)集成工具可以選擇役首,比如著名的開源軟件 jenkins,你只需要將其運(yùn)行在你自己的服務(wù)器上显拜,做出適當(dāng)?shù)呐渲眉纯珊獍拢换蛘吣阌X得麻煩,甚至可以用基于SaaS 的CI 工具远荠,比如travis.CI 以及flow.ci矮固,簡(jiǎn)單的幾分鐘就能搭建一套持續(xù)集成系統(tǒng),連設(shè)置與機(jī)器都省了譬淳。
我們將持續(xù)集成工具設(shè)置為當(dāng)新的代碼push 進(jìn)代碼倉(cāng)庫(kù)的時(shí)候档址,就運(yùn)行所有的單元測(cè)試,并最終將運(yùn)行的結(jié)果郵件通知給我們邻梆。這樣守伸,我們就可以做到既能夠足夠快速的運(yùn)行我們自己想運(yùn)行的單元測(cè)試結(jié)果,又能夠從稍后的郵件中得知整個(gè)單元測(cè)試的結(jié)果浦妄。
同時(shí)尼摹,當(dāng)你上了持續(xù)集成工具后,就算你自己不運(yùn)行測(cè)試剂娄,工具也會(huì)在你每次push 的時(shí)候忠實(shí)的運(yùn)行你之前寫的單元測(cè)試寸谜,這樣也解決了忘記跑單元測(cè)試的問題畅形。
當(dāng)持續(xù)集成工具告訴你測(cè)試失敗的時(shí)候揩环,這又回到了“當(dāng)測(cè)試出錯(cuò)的時(shí)候鸟悴,我們應(yīng)該怎么辦”的問題上了。再次啰嗦下:
一旦單元測(cè)試掛了耳胎,團(tuán)隊(duì)?wèi)?yīng)該首先解決這個(gè)問題
好了惯吕,如何堅(jiān)持單元測(cè)試的編寫就說(shuō)到了這里惕它,下次有機(jī)會(huì)說(shuō)說(shuō)持續(xù)集成工具的一些實(shí)踐。有興趣的朋友也可以在微信中搜索公眾號(hào)“持續(xù)集成慢慢來(lái)”進(jìn)行關(guān)注或留言废登,我會(huì)很高興與你交流怠缸。最后,感謝你的閱讀 : D