程序員自己寫(xiě)測(cè)試了,還要測(cè)試人員做什么?

在向開(kāi)發(fā)人員介紹單元測(cè)試或TDD等工程實(shí)踐時(shí),往往可以聽(tīng)到這樣的疑問(wèn)。比如:

自己寫(xiě)的程序,自己無(wú)法從另一個(gè)角度測(cè)出問(wèn)題。
寫(xiě)bug的時(shí)間都不夠了,哪有時(shí)間來(lái)寫(xiě)測(cè)試?
開(kāi)發(fā)來(lái)寫(xiě)測(cè)試了,測(cè)試干什么?
除了核心的代碼,沒(méi)有什么值得測(cè)試的。
……

本篇想要通過(guò)探討這些問(wèn)題背后的困難,來(lái)說(shuō)明程序員怎樣通過(guò)編寫(xiě)自測(cè)代碼更有效率的進(jìn)行開(kāi)發(fā)。

一個(gè)例子

首先我們看一個(gè)例子。


全項(xiàng)目唯一的測(cè)試

不止一次,我在各種項(xiàng)目中看到這樣的測(cè)試,往往這也是整個(gè)工程中唯一一個(gè)測(cè)試。
可以看出,開(kāi)發(fā)者認(rèn)為編寫(xiě)是有必要的。所以按照“標(biāo)準(zhǔn)”的做法建立了測(cè)試目錄,引入JUnit依賴(lài)。并且利用它在開(kāi)發(fā)的初期來(lái)驗(yàn)證某些技術(shù)疑問(wèn),一般是某些當(dāng)時(shí)還不熟悉的第三方庫(kù),或者數(shù)據(jù)庫(kù)、中間件等外部依賴(lài)。
項(xiàng)目初期技術(shù)調(diào)研階段很快過(guò)去后,似乎沒(méi)有更多需要驗(yàn)證的問(wèn)題。因而也就再?zèng)]有需要編寫(xiě)測(cè)試的地方。
簡(jiǎn)單而言:“寫(xiě)測(cè)試是應(yīng)該,但我們的代碼沒(méi)什么好測(cè)的”

測(cè)試,不僅僅關(guān)于未知

說(shuō)起測(cè)試,往往與未知相關(guān)聯(lián)。我們通過(guò)試驗(yàn)、調(diào)試、檢測(cè)來(lái)獲取獲取反饋,不斷調(diào)整。


以上圖為例,一般想到的測(cè)試,都集中在“已知的未知”這個(gè)象限。正如前面的示例代碼,使用不熟悉的庫(kù)帶來(lái)未知。程序員通過(guò)在測(cè)試中調(diào)用和觀察結(jié)果來(lái)消除未知。
然而,對(duì)于自動(dòng)化測(cè)試來(lái)說(shuō),其實(shí)關(guān)注點(diǎn)在于已知。
“都已知了,還測(cè)試什么呀?”,也許你會(huì)有這樣的疑問(wèn)。

火柴問(wèn)題


火柴,這種行將消失的物品。也許現(xiàn)在的小朋友只是在《賣(mài)火柴的小女孩》中才得知它的存在。在我小時(shí)候,還是時(shí)常用到的。那時(shí),也許是工藝問(wèn)題,或者存儲(chǔ)條件有限,往往一盒火柴好多根都不能點(diǎn)著。

記的那時(shí)聽(tīng)到的笑話(huà):
小明的媽媽讓他去買(mǎi)盒火柴,不一會(huì)功夫買(mǎi)回來(lái)了。媽媽問(wèn):“你試過(guò)沒(méi)有,能點(diǎn)著嗎?”
“試過(guò)啦”,小明很驕傲的說(shuō),“每一根我都試了一遍?!?/p>

我把這種問(wèn)題稱(chēng)為“火柴問(wèn)題”,往往傳統(tǒng)的質(zhì)量控制面臨的都是這類(lèi)問(wèn)題,有如下限制:

  • 成本,顯然現(xiàn)實(shí)中不會(huì)有人把所有的火柴拿來(lái)測(cè)試。不過(guò)問(wèn)題的本質(zhì)并沒(méi)有變,在花費(fèi)的成本和獲得安全保證的完全性之間取一個(gè)平衡。
  • 事后,造出火柴后才有能否點(diǎn)著的問(wèn)題,
    因而,
  • 一次性,成本換取的安全是一次性的,每當(dāng)一個(gè)批次到來(lái)時(shí),以前的測(cè)試的付出都成為了沉沒(méi)成本。

另一種測(cè)試

讓我們來(lái)看另一種關(guān)于已知的測(cè)試。


checklist

檢查清單。
比如每天出門(mén)的時(shí)候,我都會(huì)自然而然的檢查一遍,手機(jī)、鑰匙、錢(qián)包。就是個(gè)簡(jiǎn)單的清單。
清單是關(guān)于已知的,只有十分確定的事項(xiàng)才會(huì)列入在清單里。
清單本身很簡(jiǎn)單,并不能回答火柴問(wèn)題這樣的難題。但是不代表它沒(méi)有作用。
以出門(mén)為例子,有時(shí)出門(mén)是每天都在做的上班通勤,有時(shí)是去面臨某個(gè)很大的未知,比如去見(jiàn)一個(gè)陌生的客戶(hù),進(jìn)行重要談判。
這時(shí)如果有個(gè)水晶球,告訴你會(huì)成功失敗,甚至告訴你怎樣做才能成功,那就太好了。
然而沒(méi)有水晶球。
一個(gè)簡(jiǎn)單的清單至少保證你不會(huì)走在路上才發(fā)現(xiàn)忘帶手機(jī)。無(wú)論未知的挑戰(zhàn)是什么,忘帶手機(jī)基本上不會(huì)產(chǎn)生任何幫助。

切換回軟件開(kāi)發(fā)的場(chǎng)景,程序員夢(mèng)想中的完美測(cè)試也許能告訴我們未知,甚至未知的未知結(jié)果。這在目前還不現(xiàn)實(shí)。那么寫(xiě)一個(gè)測(cè)試確保你在不斷調(diào)整中不破壞正確的事情,仍是值得的。
可以看到,這種視角下的驗(yàn)證,與檢查火柴有所不同:

  • 預(yù)防,這種校驗(yàn)著眼于未來(lái),是為了避免更大損失的投入。
  • 過(guò)程中,檢查是做事步驟中的一個(gè)環(huán)節(jié)。
  • 反復(fù),越頻繁的行為越有必要進(jìn)行校驗(yàn),校驗(yàn)的越頻繁潛在收益越大。

假定你是獨(dú)自居住,出門(mén)前還是鎖門(mén)后發(fā)現(xiàn)沒(méi)帶鑰匙的成本,會(huì)有一個(gè)巨大的飆升。往往檢查列表都是在這種成本拐點(diǎn)前進(jìn)行的。

checklist 和成本

應(yīng)對(duì)這種猛增的成本曲線(xiàn)有三種方式:

  • 拉平曲線(xiàn),通過(guò)技術(shù)改進(jìn)使原本難以挽回的決定變得不那么昂貴。
  • 優(yōu)化待檢查項(xiàng)目,比如現(xiàn)在出門(mén)帶錢(qián)包已經(jīng)不那么重要了,有手機(jī)即可。如果把門(mén)換成掃碼開(kāi)鎖,那么鑰匙也免了。這樣需要檢查的項(xiàng)目越少,越不容易遺漏。
  • 自動(dòng)化,比如遺漏了東西就有提醒警報(bào),自然大大降低了犯錯(cuò)的可能。

自測(cè)給程序員帶來(lái)什么

敏捷方法論的一個(gè)基礎(chǔ),就是現(xiàn)代軟件開(kāi)發(fā)方式已經(jīng)使軟件變更的成本曲線(xiàn)大大平緩了。我們可以看看開(kāi)發(fā)者的自測(cè)在其中起到的作用。

錯(cuò)誤反饋等級(jí)
錯(cuò)誤定位等級(jí)

對(duì)照上面兩個(gè)列表,可以回想一下

  • 在最近的開(kāi)發(fā)活動(dòng)中碰到各類(lèi)錯(cuò)誤的比例是多少?
  • 由于反饋時(shí)間和定位手段不同,解決錯(cuò)誤花費(fèi)的時(shí)間有何不同?
  • 有多少最初百思不得其解的錯(cuò)誤,長(zhǎng)時(shí)間摸排后定位為一行修改即可改正的弱智錯(cuò)誤?
  • 如果這些錯(cuò)都在第一時(shí)間發(fā)現(xiàn),以明顯的方式報(bào)錯(cuò)會(huì)怎么樣?……

從加快反饋,幫助定位的角度思考,也許你會(huì)找到更多值得寫(xiě)的測(cè)試。

自動(dòng)化

自動(dòng)化投入時(shí)間對(duì)照表

這張表是值得花多少時(shí)間把某項(xiàng)工作自動(dòng)化,比如左上角第一格表示,一個(gè)需要一秒的操作,如果在未來(lái)5年每天執(zhí)行50次,那么花1天時(shí)間自動(dòng)化它是值得的。
事實(shí)上這張表僅僅是花費(fèi)時(shí)間的簡(jiǎn)單數(shù)學(xué)計(jì)算??紤]到注意力節(jié)省的話(huà),其實(shí)可以花費(fèi)更多的時(shí)間。
大家都可能都有過(guò)手工部署環(huán)境的經(jīng)歷,假定有10個(gè)步驟,操作只要1秒,然后等待30分鐘進(jìn)行下一步。理論上來(lái)說(shuō)這一天只需要花費(fèi)10秒在這個(gè)任務(wù)上,不過(guò)試過(guò)的人都知道,這天能有平時(shí)一半的產(chǎn)出就很不容易了。
注意力是很貴的。自動(dòng)化節(jié)省的不止是時(shí)間。

記錄

常玩游戲的同學(xué)都熟悉要時(shí)常存盤(pán),可以讓我們安心挑戰(zhàn)boss,大不了失敗時(shí)返回安全點(diǎn)。
那么代碼呢?Git,SVN等代碼管理工具使可靠的保留代碼歷史成為可能。然而,如何在歷史中找到安全點(diǎn)呢?(題外話(huà),你有嘗試過(guò)Git bisect命令么

記錄還帶來(lái)了另一件事,復(fù)盤(pán)。
沒(méi)有記錄也就無(wú)從系統(tǒng)的進(jìn)行回顧和改進(jìn)。對(duì)于編碼,我們往往只能看到最終的結(jié)果。這大概也是編碼活動(dòng)在軟件開(kāi)發(fā)“工程——藝術(shù)” 圖譜中最偏向與藝術(shù)這一極的原因吧。
頻繁提交的代碼歷史,加上表達(dá)行為變化的測(cè)試,會(huì)使原本大家熟視無(wú)睹的進(jìn)程如實(shí)呈現(xiàn)出來(lái)。有興趣的話(huà)可以看看這篇cyber-dojo設(shè)計(jì)者的講演,我們甚至僅僅觀察測(cè)試變化的情況就可以對(duì)一段程序編寫(xiě)的過(guò)程有個(gè)大致的了解。

可以通過(guò)測(cè)試改進(jìn)的點(diǎn)

把main函數(shù)改為測(cè)試

有經(jīng)驗(yàn)的開(kāi)發(fā)者大多都知道寫(xiě)出的代碼都至少要運(yùn)行驗(yàn)證一遍。然而運(yùn)行代碼有時(shí)并不那么簡(jiǎn)單,有的要以特定的方式部署,有的需要復(fù)雜的前置流程才能觸及。為了高效的運(yùn)行代碼,我們會(huì)采用一些手段,比如為目標(biāo)代碼增加一個(gè)main函數(shù),這樣就可以直接以希望的輸入執(zhí)行想要的操作,并觀察結(jié)果。
這種調(diào)試技巧可以很容易的用測(cè)試來(lái)改寫(xiě),如下圖所示。

main vs test

在基本不增加工作量的前提下,帶來(lái)如下收益:

  • 明確的分離了調(diào)試代碼和生產(chǎn)邏輯。
    避免誤導(dǎo)后來(lái)維護(hù)代碼的人,也防止把測(cè)試代碼發(fā)布到生產(chǎn)環(huán)境產(chǎn)生隱患。
  • 抹平了“調(diào)試期——維護(hù)期”的成本差異。
    main方法的往往是在調(diào)試階段使用。開(kāi)發(fā)人員反復(fù)調(diào)整輸入、觀察輸出、修正代碼,直到開(kāi)發(fā)完成。之后這段調(diào)試程序就成為了過(guò)去時(shí)。后來(lái)者無(wú)法判斷這段腳手架代碼是否還符合最新的邏輯,是否可以運(yùn)行。
    而測(cè)試代碼在每次構(gòu)建時(shí)都會(huì)自動(dòng)檢查,保證代碼保持上次變更后預(yù)期的邏輯。為開(kāi)發(fā)者保留了一個(gè)調(diào)試現(xiàn)場(chǎng),是否“開(kāi)發(fā)完了”并無(wú)顯著差異。
  • 測(cè)試可以記錄多種用例
    使用調(diào)試方式,我們往往在確認(rèn)完一個(gè)行為后修改輸入,觀察其它行為。因?yàn)轭A(yù)期這是一次性的工作。
    用測(cè)試可以在不同的用例中描述行為的不同側(cè)面。方便維護(hù)者理解代碼,也避免了,“咦,這個(gè)bug我明明測(cè)過(guò)呀”的回歸錯(cuò)誤。
  • 測(cè)試明確寫(xiě)出了期望的行為。
    通過(guò)assert,測(cè)試明確的寫(xiě)出可以自動(dòng)判別的行為。而不是main方法中通過(guò)肉眼來(lái)閱讀理解程序行為。寫(xiě)出預(yù)期會(huì)帶來(lái)如下改變:
    • 幫助閱讀者理解什么是代碼“應(yīng)該的”行為。
    • 促使開(kāi)發(fā)者思索代碼的目的是什么,會(huì)怎樣被使用。
    • 自動(dòng)判斷節(jié)省了開(kāi)發(fā)者的注意力,更有效的反饋錯(cuò)誤,定位錯(cuò)誤。

用隔離依賴(lài)代替調(diào)試“高仿”代碼

所謂高仿代碼,是指與實(shí)現(xiàn)代碼非常接近,但是稍有不同的代碼。
往往在調(diào)試時(shí),目標(biāo)代碼并不是純粹的邏輯處理,還會(huì)涉及到其它的外部依賴(lài)。這些依賴(lài)可能要單獨(dú)部署配置,甚至根本無(wú)法在開(kāi)發(fā)環(huán)境獲得。
為了對(duì)付這種情況,一個(gè)顯而易見(jiàn)的方法是把目標(biāo)代碼copy一份到調(diào)試代碼處,修改依賴(lài)相關(guān)的部分。比如下圖就演示了一段代碼,需要根據(jù)外部依賴(lài)判斷執(zhí)行某操作,并更新數(shù)據(jù)庫(kù)。為了測(cè)試執(zhí)行操作的邏輯,開(kāi)發(fā)者copy了代碼,注釋掉與環(huán)境相關(guān)的代碼。

copy code vs test

另一種類(lèi)似的處理方法,在每次調(diào)試時(shí)臨時(shí)修改目標(biāo)代碼,調(diào)試結(jié)束后再恢復(fù)。

這種情況,只需要結(jié)合mock框架對(duì)外部依賴(lài)進(jìn)行模擬,就可以在不改變目標(biāo)代碼的情況下在測(cè)試中改變代碼行為。如上圖所示。
這種做法有避免了顯而易見(jiàn)的問(wèn)題:

  • copy代碼方式在經(jīng)歷修改后,不能保證于實(shí)際生產(chǎn)代碼一致。
  • 臨時(shí)修改代碼有事后忘記恢復(fù)的風(fēng)險(xiǎn)。

除此之外,還有些潛移默化的收益:

  • 使隱含的輸入輸出更加明顯了。
    比如例子中的代碼,從外部看起來(lái)只有一個(gè)字符串輸入一個(gè)字符串輸出。通過(guò)測(cè)試可以明確的看到,事實(shí)上輸入還有從外部依賴(lài)獲取的布爾值,輸出還有對(duì)數(shù)據(jù)庫(kù)的操作。
  • 促使代碼向松耦合、單一職責(zé)演化。
    有時(shí)候?yàn)榱嗽跍y(cè)試中mock隔離依賴(lài),會(huì)需要對(duì)實(shí)現(xiàn)代碼稍作重構(gòu)。短期看來(lái)似乎寫(xiě)測(cè)試引發(fā)了更多的工作量和變更,但這種變更一般會(huì)使代碼向職責(zé)更明確,模塊間更松耦合的方向改變。促使開(kāi)發(fā)者在設(shè)計(jì)層面更多的思考。

用測(cè)試來(lái)增強(qiáng)注釋

適當(dāng)?shù)淖⑨屇軜O大的增強(qiáng)代碼的可維護(hù)性。好的注釋描述代碼在做什么,而非怎么做的。
對(duì)于復(fù)雜結(jié)構(gòu)的處理,往往看代碼千頭萬(wàn)緒,摸不著頭腦。注釋里附上示例數(shù)據(jù),馬上讓人對(duì)代碼的大致行為有所掌握。

comments vs test

將這種注釋中的樣例放入測(cè)試中,可以:

  • 避免代碼修改注釋無(wú)人維護(hù)的問(wèn)題。
  • 把不同的輸入和對(duì)應(yīng)輸出一一對(duì)應(yīng)起來(lái)。

利用自測(cè)促進(jìn)開(kāi)發(fā)

前面說(shuō)了一些通過(guò)自測(cè)手段對(duì)已有工作方式的改進(jìn)。事實(shí)上在熟悉掌握這些手段后,可以更進(jìn)一步,主動(dòng)利用測(cè)試來(lái)完成原來(lái)不能高效做到的事情。

分解“已知的未知”

對(duì)于未知的解決方案,有時(shí)是由于我們對(duì)于相關(guān)技術(shù)了解有限。也有一種情況,技術(shù)方面已經(jīng)確定,但是由于問(wèn)題較為復(fù)雜,一時(shí)看不到解決方法。
面對(duì)這種問(wèn)題,一般的做法是構(gòu)造式的。也就是說(shuō)從自己知道的方案出發(fā),看看需要增加什么來(lái)接近目標(biāo),增加后調(diào)整使整體一致,再次看需要增加什么……
還有一種分解式的方式。假定已經(jīng)有了一個(gè)解決方案,從中選取一個(gè)子集,解決這個(gè)子集,然后選取下一個(gè),直到完全解決。測(cè)試就很適合在這種方法中對(duì)問(wèn)題進(jìn)行分解和檢驗(yàn)。

在最近的一次練習(xí)中,我就體會(huì)到即使沒(méi)有開(kāi)始編碼,測(cè)試也能對(duì)解決問(wèn)題起到幫助。

練習(xí):寫(xiě)一個(gè)函數(shù),判斷兩個(gè)字符串是否同構(gòu)。
所謂同構(gòu),是指字符串A可以通過(guò)字符替換變?yōu)樽址瓸。
比如

  • Hello 與 Apple,不同構(gòu)
  • Hello 與 Speed,同構(gòu)

有興趣的同學(xué)可以自己嘗試嘗試,能否通過(guò)測(cè)試逐步分解問(wèn)題找到解決方案。
提示:從最簡(jiǎn)單確定的問(wèn)題開(kāi)始,比如一個(gè)字母的字符串如何判斷。

顯現(xiàn)“未知的已知”

有多少次,當(dāng)你正在開(kāi)發(fā)調(diào)試的過(guò)程中,發(fā)現(xiàn)了某種更好的做法。然而思索后你對(duì)自己說(shuō):“已經(jīng)差不多寫(xiě)好了,算了,還是以后再改吧”。即便這個(gè)改動(dòng)只是給函數(shù)起個(gè)更貼切的名字而已。
而我們都知道,以后往往等于永遠(yuǎn)也不會(huì)。

造成這種狀況的,除了我們固有的弱點(diǎn),比如拖延、圖省事外,有個(gè)很重要的原因是難以評(píng)估改變的影響。還記的前面錯(cuò)誤反饋列表么?如果幾個(gè)月后才會(huì)知道有沒(méi)有問(wèn)題的改動(dòng),就算再簡(jiǎn)單我們也會(huì)避免的。這就是遺留代碼的處境。

眾所周知,不產(chǎn)生bug的最佳方式就是不寫(xiě)、不修改代碼。當(dāng)然這是不現(xiàn)實(shí)的。所以會(huì)有兩種局部化變更影響的方式。

原木式
碼出的結(jié)構(gòu)

不同的用例邏輯好像木材一樣碼在一起,彼此類(lèi)似又稍有不同。
好處顯而易見(jiàn),新增一條木頭并不會(huì)影響另一條木頭。
缺陷是出現(xiàn)切片式的變更時(shí)會(huì)發(fā)生霰彈式修改。隨著代碼歷史變長(zhǎng)每條木頭間的微妙差異會(huì)越來(lái)越難以分辨是無(wú)意的不同步,還是有業(yè)務(wù)含義的特性。

沉淀式
沉淀出的結(jié)構(gòu)

在有控制的搖動(dòng)/靜置中。不同關(guān)注點(diǎn)的邏輯逐步分層,基礎(chǔ)的邏輯越來(lái)越沉淀到下方,越來(lái)越穩(wěn)定。易變的邏輯浮在頂層,但是影響的范圍越來(lái)越少。
缺少控制的情況下,這種組織方式是不可行的。足夠的測(cè)試正是用來(lái)顯現(xiàn)和保持這種沉淀的必要條件。

說(shuō)走就走的旅行

回到標(biāo)題的問(wèn)題,程序員為什么要自測(cè),與測(cè)試人員所做測(cè)試的區(qū)別。

測(cè)試人員更多著眼于火柴問(wèn)題式的未知,關(guān)于軟件在不確定的使用中是否達(dá)到預(yù)期的效用。
開(kāi)發(fā)人員的自測(cè)更多著眼于檢查清單式的已知,關(guān)于軟件在不確定的修改中是否保持已知的行為。

盡管并不直接回答未知的問(wèn)題,掌握已知,是我們應(yīng)對(duì)未知的保證。
就像背包探險(xiǎn)的旅行家。組織有序的行囊,是說(shuō)走就走,去向未知風(fēng)景的保證。

  • 驗(yàn)證已知,讓機(jī)器幫助檢驗(yàn),為了更好的探索未知。
  • 測(cè)試是為了更好的改變,而不是防止改變。
  • 多個(gè)簡(jiǎn)單、具體的特例,可以描述復(fù)雜、一般化的邏輯。

有評(píng)論、疑問(wèn)或建議,歡迎加群深入探討。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,872評(píng)論 25 709
  • 1、通過(guò)CocoaPods安裝項(xiàng)目名稱(chēng)項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫(kù)組件 SD...
    陽(yáng)明AI閱讀 16,186評(píng)論 3 119
  • 【0506晨讀感想】 如何提高自己的橫向領(lǐng)導(dǎo)力? 顧名思義,縱向領(lǐng)導(dǎo)力,說(shuō)的是上下級(jí)之間的領(lǐng)導(dǎo)關(guān)系;那么橫向領(lǐng)導(dǎo)力...
    小二關(guān)閱讀 150評(píng)論 0 0
  • 溫水煮青蛙。青蛙是在溫水里一點(diǎn)點(diǎn),慢慢地煮死的。現(xiàn)在是沒(méi)有痛苦,沒(méi)有痛楚,時(shí)間長(zhǎng)了,就一點(diǎn)點(diǎn)死掉的。只是特別和我們...
    喬沫閱讀 462評(píng)論 0 0

友情鏈接更多精彩內(nèi)容