論測試的重要性
如今程序員群體趕上了中國最龐大的農(nóng)民
群體,大街上隨便抓一把,十有八九是程序員,還一個(gè)剛從某國企離職報(bào)名參加軟件培訓(xùn)班。我想碼農(nóng)
的稱號(hào)或許就是這么來的吧。
在外行人看來,程序員是一個(gè)成天對(duì)著電腦倒騰著代碼、看著Terminal上行云流水般的打印、過著不修邊幅的日子外加超負(fù)荷的碼農(nóng)。
在內(nèi)行人看來,程序員是一個(gè)成天面對(duì)QA的"質(zhì)疑"、PM的"奪命催"以及DEVs的"吐槽",扛著身心壓力的苦行僧
。
在我看來,程序員應(yīng)該是:
手持神劍,心懷善念,胸有成竹、有理有據(jù)并且合情合理地跟QA、PM、DEV斗智斗勇的戰(zhàn)士。
要擺脫QA的質(zhì)疑、DEVs的吐槽以及PM的奪命催,除了那些不容易掌控的客觀因素,我們可以從自身發(fā)力,加強(qiáng)自身的"核心肌群"
,呈現(xiàn)出自己的應(yīng)有的專業(yè)態(tài)度,編寫出高質(zhì)量的代碼,從而促成高質(zhì)量的交付。
如何交付高質(zhì)量的代碼?
首先,我們可以擺出苦行僧的心態(tài),平日里練就一身好把式:如Clean Code、Refactor、OOD及FOP。即便這樣,牛逼哄哄的程序員也不敢說自己的代碼百分之百?zèng)]有缺陷。
怎么辦,兩個(gè)參考原則:
- 編寫完代碼多問自己一句:"真的可靠地完成目標(biāo)了嗎?" 怎么問,寫個(gè)測試來提問。這便是 測試覆蓋。
- 編寫代碼之前先問自己一句:"怎么樣才算完成目標(biāo)了呢?" 怎么問,同樣寫個(gè)測試來提問。這便是 TDD + 測試覆蓋。
測試能做什么
要知道測試能做什么,首先我們需要知道測試是什么(它在測什么)?它能給我們帶來什么價(jià)值?以及人力成本那么昂貴,我們?yōu)槭裁催€要花時(shí)間去編寫這些上不了產(chǎn)品的測試代碼?
程序員總喜歡倒騰點(diǎn)代碼來開始一個(gè)話題:
public class StringUtils {
public static String toUpperCase(String source) {
if (source == null) {
return null;
}
return source.toUpperCase();
}
}
class StringUtilsTest {
@Test
void convert_to_upper_case() throws Exception {
assertThat(StringUtils.toUpperCase("unit-test"), is("UNIT-TEST"));
}
}
這一小段測試代碼所做的事情是在驗(yàn)證StringUtils#toUpperCase
方法的功能正確性。
順便用一句話來形容單元測試:
開發(fā)人員編寫一小段代碼,用于檢驗(yàn)被檢測代碼的一個(gè)很小的、很明確的功能是否正確。
廣義上的測試并不總是像上面這段代碼這么簡單,熟為人知的 測試金字塔 將測試分為三大類,單元測試位于測試金字塔底端,旨在傳達(dá)單元測試應(yīng)該來得更兇猛一些,而它們正是由開發(fā)人員親手編寫出來。本文也是圍繞單元測試來開展。
測試的價(jià)值何在
經(jīng)常聽開發(fā)人員說:"我對(duì)我的代碼非常有信心。"理由往往充分且單一:單元測試是老大,老大罩著我不怕。(當(dāng)然,專業(yè)的QA始終能發(fā)現(xiàn)DEV很難察覺到的Defect,難免會(huì)驚起一臉狐疑:老大不靈了嗎!回首代碼,覺漏某一Case)。
所以單元測試能夠增強(qiáng)你寫代碼的信心。都說自信是成功者必不可少的特質(zhì)。當(dāng)你對(duì)代碼充滿信心之后,你的潛能無形中被激發(fā)(你會(huì)發(fā)現(xiàn)你敲代碼的速度都會(huì)變快),這樣你工作效率的提高促使你更加輕松地完成工作。身心受益便會(huì)產(chǎn)生一連串良性的"蝴蝶效應(yīng)"。
測試的兩個(gè)無形價(jià)值就體現(xiàn)出來了:
1. 增強(qiáng)我們寫代碼的信心。
2. 讓我們更加輕松的完成工作,身心收益。
再來說說有形的代碼。缺陷減少了則證明你的代碼質(zhì)量提高了,代碼質(zhì)量衡量指標(biāo)總離不開可讀性
、可擴(kuò)展性
、可維護(hù)性
。這三個(gè)指標(biāo)的增強(qiáng)反映了良好的代碼整潔度、OO設(shè)計(jì)、模塊化等。實(shí)踐證明,這些良好的設(shè)計(jì)往往不是一蹴而就的,而當(dāng)你為一個(gè)類或方法編寫單元測試卻舉步維艱的時(shí)候,你就應(yīng)該考慮去改良你的設(shè)計(jì)了。
理想情況下,編寫完的代碼應(yīng)該是可以工作的。但現(xiàn)實(shí)并不那么美好,當(dāng)你在驗(yàn)證代碼正確性的時(shí)候遇到問題,你就不得不頻繁地啟用調(diào)式模式,而調(diào)試正是吞噬你寶貴時(shí)間的惡魔。此時(shí)我們要拔出單元測試這把神劍,使出渾身解數(shù)將惡魔驅(qū)趕到塵封的黑暗角落,從而縮減我們花在調(diào)式上的時(shí)間。
那么,測試的兩個(gè)有形的價(jià)值也體現(xiàn)出來了:
3. 改良我們的設(shè)計(jì)。
4. 縮減我們花在調(diào)式上的時(shí)間。
在敏捷開發(fā)領(lǐng)域,文檔(需求文檔,詳細(xì)設(shè)計(jì)文檔等)是罕見之物。當(dāng)一個(gè)新人半途加入項(xiàng)目的時(shí)候,在沒有太多文檔的情況下,閱讀測試代碼便是一個(gè)很好的開始。當(dāng)然,前提是我們的測試代碼必須是可靠的,并且具有良好的可讀性。單元測試的第五項(xiàng)不可小覷的價(jià)值就被體現(xiàn)出來:
5. 測試即文檔。
不寫測試又如何
有一種聲音:"單元測試代碼寫得再漂亮,也終究不是產(chǎn)品代碼,在部署到生產(chǎn)環(huán)境時(shí)會(huì)被無情的拋棄掉!"
所以被這種聲音迷惑的人開始信奉了長(測)話(試)短(少)說(寫),短(甚)話(至)不(不)說(寫)的信仰。這只是經(jīng)過修飾得以傳播的一種聲音,而背后做支撐的總有那么幾大派系。
無辜派
1. 我并不清楚代碼的行為,你叫我怎么測試呢?
2. 這些代碼命名都能夠通過編譯啊!
個(gè)性派
1. 測試代碼不是我的工作,這不應(yīng)該由專門的人去做嗎?
2. 公司請(qǐng)我來是為了寫代碼,而不是寫測試的。
同理派
1. 如果我讓QA人員沒有工作,那么我會(huì)覺得很內(nèi)疚的!
仔細(xì)推敲這三大派系,甩出幾個(gè)問題就能讓這些借口不攻自破:
1. 如果連代碼的行為都不清楚,寫出來的代碼意義何在?
2. 通過編譯就代表能正常工作嗎?
3. 你可以不寫測試,但你寫的代碼不斷被QA找出Defect,作為DEV名聲信譽(yù)何在,難道寫出可靠的代碼也不是你的職責(zé)嗎?
4. 公司的確不是雇你來寫測試的,那公司是顧你來調(diào)試bug的嗎?
5. 試問QA會(huì)喜歡一個(gè)交付的代碼存在很多Defect的DEV嗎?我想QA也寧愿代碼可靠到讓他ta"無事可做",從而去做一些功能測試、性能測試、驗(yàn)收測試等。
讓我覺得值得一提的是常規(guī)派的看法:
1. 編寫單元測試太花時(shí)間了,項(xiàng)目結(jié)束時(shí)再說吧!
2. 運(yùn)行測試時(shí)間太長了!
"編寫單元測試太花時(shí)間了,等測試結(jié)束后再說" 聽起來是一個(gè)很合乎情理的想法。而在軟件開發(fā)項(xiàng)目上存在一個(gè)這樣的魔咒:
一推再推的事情,往往都是不會(huì)去做的事情。
不去做的原因可能是重視度不夠,被和諧掉了,也可能是最后想去做也沒有時(shí)間去做。不管出于什么原因,不寫測試存在潛在的風(fēng)險(xiǎn)。
實(shí)踐證明,隨著時(shí)間推移,產(chǎn)品的功能性的變化趨勢受測試代碼編寫的時(shí)機(jī)的影響如下圖所示:
好想法抵擋不住現(xiàn)實(shí)的打擊,代碼庫隨著項(xiàng)目的進(jìn)展越發(fā)復(fù)雜,由于沒有測試的保護(hù),一些不良的設(shè)計(jì)偷偷溜了進(jìn)來,代碼越發(fā)嬌氣,慢慢地沒有人敢去動(dòng)它。最糟糕的結(jié)果可能是,DEVs頂著巨大交付壓力,唯唯諾諾的寫著代碼,而災(zāi)難正在醞釀,交付最終失敗。
所以,只有當(dāng)測試代碼并行于產(chǎn)品代碼,甚至可以采用TDD。測試的幾大價(jià)值才有可能被體現(xiàn)出來,從而能夠?yàn)槲覀兊漠a(chǎn)品保駕護(hù)航。
就我個(gè)人經(jīng)驗(yàn),半TDD的編碼方式,在一個(gè)Story上所花的總時(shí)間不會(huì)多余沒有測試裸奔的代碼。或許剛開始會(huì)覺得有點(diǎn)拖慢節(jié)奏,操練多了,它的威力就會(huì)彰顯出來了。
測試也寫了,可是運(yùn)行時(shí)間太長了又帶來了另一個(gè)苦惱?
細(xì)談該苦惱可以單獨(dú)寫一篇文章了。我的確見過測試運(yùn)行時(shí)間很長,每次驗(yàn)證一次跑上半個(gè)多小時(shí)。下面列舉一些測試加速的實(shí)踐:
1. 編寫更多的單元代碼來代替一些不重要的集成測試和UI測試。
2. 使用Mockito、JMock等工具模擬掉依賴。
3. 并行運(yùn)行測試,前提是讓測試之間保持相互獨(dú)立。
4. 讓CI服務(wù)器去跑更耗時(shí)的集成測試和UI測試。
5. 使用契約測試來代替微服務(wù)之間的集成測試。
單元測試運(yùn)行時(shí)間是毫秒級(jí)別的,如果耗時(shí)過長,你就要留意是否存在內(nèi)存泄漏、資源未釋放、依賴過重或者不依賴容器而啟動(dòng)了容器的單元測試。
揮之不去的例外
編寫單元測試是一項(xiàng)成本低卻價(jià)值很高的活動(dòng)。編寫它不會(huì)花掉你太多的時(shí)間,而運(yùn)行它更是毫秒間的事情。極限編程推崇者正在使用TDD的方式詮釋著單元測試的價(jià)值和意義。
它能帶給我們信心,改良我們的代碼設(shè)計(jì),提升我們(DEVs)的聲譽(yù),為代碼庫保駕護(hù)航,為高質(zhì)量的軟件交付提供保障。但它終究不是一顆銀彈。我們編寫單元測試也無非是一種價(jià)值的取舍,當(dāng)它給我們帶來的價(jià)值低于我們付出的成本時(shí),我們就要保持警惕了,比如思考以下兩個(gè)問題:
1. 在追求漂亮的測試覆蓋率數(shù)字100%的時(shí)候,思考一下它真有那么高的價(jià)值嗎?
2. 在做快速的技術(shù)Spike(技術(shù)調(diào)研),思考一下不寫測試是不是能讓我更快的試錯(cuò)?
我們要理解的是單元測試背后的核心價(jià)值,從而做出正確的取舍。我們要做的是編寫出有效的單元測試,讓它真正地為我們創(chuàng)造價(jià)值。
注釋
- Terminal:命令行終端
- QA:專職測試人員
- PM:項(xiàng)目經(jīng)理
- DEV:開發(fā)人員,DEVs表示復(fù)數(shù)
- OOD:面向?qū)ο笤O(shè)計(jì)
- FOP:函數(shù)時(shí)編程
- TDD:測試驅(qū)動(dòng)開發(fā)
- CI:持續(xù)集成