前言
本文搜集了Henrik Warne一個(gè)20年編程老手的個(gè)人經(jīng)驗(yàn)教訓(xùn)總結(jié),文中類(lèi)容包括他分享回顧了的194個(gè)bug(時(shí)間跨度達(dá)13年)。下面是最重要的幾個(gè)經(jīng)驗(yàn)教訓(xùn),分為編碼、測(cè)試和調(diào)試這三大類(lèi)。
Henrik Warne是瑞典斯德哥爾摩的一名軟件開(kāi)發(fā)員。目前,他是TriOptima公司的開(kāi)發(fā)員,負(fù)責(zé)為財(cái)務(wù)風(fēng)險(xiǎn)管理系統(tǒng)開(kāi)發(fā)軟件。一個(gè)20年編程老手將他的個(gè)人經(jīng)驗(yàn)分享出來(lái),希望對(duì)大家有所幫助。
編碼
這些是在過(guò)去給我?guī)?lái)棘手bug的所有問(wèn)題:
1. 事件順序:處理事件時(shí),很有必要提出下列問(wèn)題:事件是否可以以一種不同的順序到達(dá)?如果我們從來(lái)沒(méi)有收到該事件,會(huì)怎樣?如果該事件連續(xù)出現(xiàn)兩次,又會(huì)怎樣?即使通常情況下這永遠(yuǎn)不會(huì)出現(xiàn),但系統(tǒng)(或交互系統(tǒng))的其他部分中的bug可能會(huì)導(dǎo)致這出現(xiàn)。
2. 處理太早:這是上述“事件順序”的一種特殊情況,不過(guò)它已引起了一些棘手的bug,所以它自成一類(lèi)。比如說(shuō),如果信令消息接收太早,在配置和啟動(dòng)過(guò)程完成之前接收,許多奇怪的行為就會(huì)出現(xiàn)。另一個(gè)例子:當(dāng)某個(gè)網(wǎng)絡(luò)連接還沒(méi)有被列入空閑列表就被標(biāo)為斷開(kāi)。調(diào)試這個(gè)問(wèn)題時(shí),我們總是假設(shè)它在處于空閑列表時(shí)被設(shè)為斷開(kāi)(但為什么它又沒(méi)有從列表上撤下?)。沒(méi)考慮到有時(shí)動(dòng)作發(fā)生太早要怪我們沒(méi)想到。
3. 隱蔽故障:一些跟蹤起來(lái)最棘手的bug(一方面)是由出現(xiàn)隱蔽故障、繼續(xù)執(zhí)行而不是給出錯(cuò)誤的代碼引起的。比如說(shuō),系統(tǒng)調(diào)用(比如綁定)返回未加檢查的錯(cuò)誤代碼。另一個(gè)例子:遇到錯(cuò)誤元素后,直接返回而不是給出錯(cuò)誤的解析代碼。調(diào)用在故障狀態(tài)下繼續(xù)持續(xù)一段時(shí)間,這大大加大了調(diào)試的難度。最好一旦檢測(cè)到故障情況,就返回錯(cuò)誤。
4. if語(yǔ)句:有幾個(gè)條件的if語(yǔ)句給我?guī)?lái)了許多bug。即使if語(yǔ)句概念上很簡(jiǎn)單,有多個(gè)條件需要跟蹤時(shí),它們也很容易搞錯(cuò)。如今我試著重寫(xiě)代碼,力求更簡(jiǎn)單,避免要處理復(fù)雜的if語(yǔ)句。
5. Else:有幾個(gè)bug是沒(méi)有適當(dāng)考慮如果條件為假會(huì)發(fā)生什么而引起的。幾乎無(wú)一例外的是,每個(gè)if語(yǔ)句應(yīng)該有一個(gè)else部分。此外,如果你在if語(yǔ)句的一個(gè)分支中設(shè)置了某個(gè)變量,可能應(yīng)該在另一個(gè)分支也要設(shè)置該變量。與此相關(guān)的是標(biāo)志(flag)被設(shè)定的情況。僅僅添加設(shè)定標(biāo)志的條件很容易,但是容易忘了添加應(yīng)該重新設(shè)定標(biāo)志的條件。任由永久性設(shè)定的標(biāo)志留在那里可能會(huì)在將來(lái)導(dǎo)致bug。
6. 不斷變化的假設(shè):一開(kāi)始最難預(yù)防的許多bug是由不斷變化的假設(shè)引起的。比如說(shuō),一開(kāi)始,可能每天只有一個(gè)客戶事件。然后,按照這種假設(shè)編寫(xiě)了許多代碼。后來(lái)某個(gè)時(shí)候,設(shè)計(jì)發(fā)生了變化,允許每天有多個(gè)客戶事件。出現(xiàn)這種情況后,就很難改變受到新設(shè)計(jì)影響的所有情況。很容易找到顯式依賴(lài)該變化的所有項(xiàng),但是難就難在,找到隱式依賴(lài)舊設(shè)計(jì)的所有情況。比如說(shuō),可能有代碼讀取某一天的所有客戶事件。隱式的假設(shè)可能是,結(jié)果集從不大于客戶數(shù)量。我沒(méi)有好的辦法可以預(yù)防這類(lèi)問(wèn)題,歡迎讀者建議。
7. 日志:深入了解程序執(zhí)行的任務(wù)至關(guān)重要,尤其是邏輯很復(fù)雜時(shí)。務(wù)必要添加足夠多(但是別太多)的日志,那樣你就能弄清楚為什么程序在執(zhí)行它執(zhí)行的任務(wù)。如果一切正常,日志并不重要,但是一旦出現(xiàn)了問(wèn)題(這不可避免),你會(huì)很高興添加了適當(dāng)?shù)娜罩居涗洝?/p>
測(cè)試
作為一名開(kāi)發(fā)者,除非進(jìn)行了測(cè)試,否則我不會(huì)說(shuō)搞完了一項(xiàng)功能。至少,這意味著每一行新代碼或更改后的代碼至少執(zhí)行了一次。此外,單元測(cè)試或功能測(cè)試也很好,但還不夠。新功能還必須在類(lèi)似生產(chǎn)環(huán)境的環(huán)境下加以測(cè)試和探究。下面是bug在測(cè)試方面給予我的一些重要的經(jīng)驗(yàn)教訓(xùn):
8. 零和空:務(wù)必要以零和空(合適的情況下)來(lái)進(jìn)行測(cè)試。對(duì)于字符串而言,這意味著既指長(zhǎng)度為零的字符串,又指內(nèi)容為空的字符串。另一個(gè)例子:在發(fā)送任何數(shù)據(jù)(零字節(jié))之前,測(cè)試TCP連接的斷開(kāi)。沒(méi)有使用這些組合來(lái)測(cè)試是bug悄然出現(xiàn)的頭號(hào)原因,我在測(cè)試時(shí)是原本可以發(fā)現(xiàn)這些bug的。
9. 添加和刪除:新功能常常需要能夠?yàn)橄到y(tǒng)添加新配置,比如說(shuō)用于電話號(hào)碼翻譯的新配置文件。所以測(cè)試它切實(shí)可行、以便添加新的配置文件很自然不過(guò)。然而,我發(fā)現(xiàn)很容易忘了還要測(cè)試配置文件的刪除。
10. 錯(cuò)誤處理:處理錯(cuò)誤的代碼常常很難測(cè)試。最好由自動(dòng)測(cè)試來(lái)檢查錯(cuò)誤處理代碼,但有時(shí)這不可能。這種情況下,我有時(shí)采用的一招就是,臨時(shí)修改代碼,讓錯(cuò)誤處理代碼運(yùn)行。要做到這一點(diǎn),最容易的方法就是反轉(zhuǎn)if語(yǔ)句,比如說(shuō)將if語(yǔ)句由error_count > 0反轉(zhuǎn)為error_count == 0。另一個(gè)例子是誤拼數(shù)據(jù)庫(kù)列名,讓所需的錯(cuò)誤處理代碼運(yùn)行。
11. 隨機(jī)性輸入:常常可以發(fā)現(xiàn)bug的一種測(cè)試方法就是使用隨機(jī)性輸入。比如說(shuō),H.323協(xié)議的ASN.1解碼可處理二進(jìn)制數(shù)據(jù)。通過(guò)發(fā)送有待解碼的隨機(jī)性字節(jié),我們發(fā)現(xiàn)了解碼器中的幾個(gè)bug。另一個(gè)例子是使用測(cè)試調(diào)用生成腳本,其中調(diào)用持續(xù)時(shí)間、回復(fù)延遲、第一方掛斷等都是隨機(jī)生成的內(nèi)容。這些測(cè)試腳本暴露了無(wú)數(shù)bug,尤其是接踵而至的事件引起的干擾。
12. 檢查什么不該發(fā)生:測(cè)試常常包括檢查所需的動(dòng)作已發(fā)生。但它很容易忽視相反的情況――檢查不該發(fā)生的動(dòng)作確實(shí)沒(méi)有發(fā)生。
13. 自行編寫(xiě)工具:我通常構(gòu)建自己的小工具,好讓測(cè)試更容易。比如說(shuō),我在處理面向VoIP的SIP協(xié)議時(shí),寫(xiě)了一個(gè)小腳本,就返回我所需要的頭和值。有了這個(gè)工具,許多個(gè)別情況測(cè)試起來(lái)很容易。另一個(gè)例子是可以進(jìn)行API調(diào)用的命令行工具。通過(guò)從小處著手,然后根據(jù)需要逐步添加功能,我最后開(kāi)發(fā)出了非常實(shí)用的工具。自行編寫(xiě)工具的好處就是,我獲得了所需的那種功能。
不過(guò)根本不可能在測(cè)試中發(fā)現(xiàn)所有bug,有一回,我改變了由兩部分組成的處理關(guān)聯(lián)號(hào)碼的機(jī)制:路由地址前綴(始終一樣),以及從000到999的動(dòng)態(tài)分配號(hào)碼。問(wèn)題是,查找關(guān)聯(lián)時(shí),動(dòng)態(tài)分配號(hào)碼的第一位數(shù)字在查詢地址表之前就被誤刪除了。所以,不是尋找637之類(lèi)的號(hào)碼,你尋找的是37,而這個(gè)號(hào)碼不在表中。這意味著,它一直尋找到100,所以前100個(gè)調(diào)用正常,而之余的所有900個(gè)調(diào)用失效。所以除非我在重新啟動(dòng)之前測(cè)試了100多次,否則在測(cè)試時(shí)發(fā)現(xiàn)不了這個(gè)問(wèn)題。
調(diào)試
14. 討論:在過(guò)去對(duì)我?guī)椭畲蟮恼{(diào)試方法就是與同事討論問(wèn)題。我常常只要向同事描述問(wèn)題,就足以認(rèn)識(shí)到問(wèn)題是什么。此外,即使同事不是很熟悉相應(yīng)代碼,常常也能給出好主意,表明哪里可能有問(wèn)題。我在處理最棘手的bug時(shí),與同事討論這一招來(lái)得尤其管用。
15. 密切關(guān)注:調(diào)試某個(gè)問(wèn)題花很長(zhǎng)時(shí)間時(shí),常常是由于我做了錯(cuò)誤的假設(shè)。比如說(shuō),我以為問(wèn)題出現(xiàn)在某個(gè)方法中,而實(shí)際上這個(gè)問(wèn)題根本不會(huì)出現(xiàn)在這個(gè)方法中?;蛘邟伋龅漠惓2⒉皇俏壹僭O(shè)的那個(gè)異常?;蛘呶乙詾樵谶\(yùn)行軟件的最新版本,實(shí)際上運(yùn)行的是舊版本。因此,一定要核實(shí)這些細(xì)節(jié),而不是犯想當(dāng)然的毛病。很容易看見(jiàn)預(yù)期看見(jiàn)的問(wèn)題,而不是實(shí)際擺在那里的問(wèn)題。
16. 最近的變化:過(guò)去可以運(yùn)行的代碼現(xiàn)在無(wú)法運(yùn)行時(shí),這常常是最后一個(gè)變更的對(duì)象引起的。有一回,最近變化的對(duì)象只是日志,但是日志中的錯(cuò)誤引起了更大的問(wèn)題。為了讓諸如此類(lèi)的回歸更容易找到,有必要在不同的提交代碼中實(shí)行不同的變更,并且要清楚說(shuō)明變更。
17. 相信用戶:有時(shí)候用戶報(bào)告問(wèn)題時(shí),我的本能反應(yīng)是“這不可能。他們肯定是哪里弄錯(cuò)了?!钡俏乙褜W(xué)會(huì)了擯棄這樣的反應(yīng)。結(jié)果往往證明,用戶報(bào)告的正是實(shí)際發(fā)生的問(wèn)題。所以如今,我對(duì)用戶報(bào)告的問(wèn)題信以為真。當(dāng)然,我仍反復(fù)核查各方面已正確設(shè)定。但是我碰過(guò)好多情況下,之所以發(fā)生奇怪的問(wèn)題,是由于不同尋常的配置或意料之外的使用,而我默認(rèn)的假設(shè)是,它們是正確的,程序是錯(cuò)誤的。
18. 測(cè)試修正版:bug的修正版準(zhǔn)備就緒后,它必須進(jìn)行測(cè)試。先在沒(méi)有修正版的情況下運(yùn)行代碼,觀察bug。然后打上修正版,重復(fù)測(cè)試用例?,F(xiàn)在,錯(cuò)誤行為應(yīng)該消失。遵照這些步驟可以確保它其實(shí)是個(gè)bug,確保修正版確實(shí)解決了問(wèn)題。這很簡(jiǎn)單,又必不可少。
其他意見(jiàn)
這13年來(lái)我一直在跟蹤我遇到的最棘手的bug,這期間發(fā)生了很大的變化。我開(kāi)發(fā)過(guò)一個(gè)小型嵌入式系統(tǒng)、一個(gè)大型電信系統(tǒng)以及一個(gè)基于Web的系統(tǒng)。我用C 、Ruby、Java和Python編寫(xiě)過(guò)代碼。我用C 編碼時(shí)期的幾類(lèi)bug已完全消失,比如堆棧溢出、內(nèi)存損壞、字符串問(wèn)題以及某些形式的內(nèi)存泄漏。
我遇到的其他問(wèn)題(比如循環(huán)錯(cuò)誤和個(gè)別情況)少了很多,那是由于我一直對(duì)更多的邏輯進(jìn)行單元測(cè)試。但是,這并不意味著沒(méi)有bug,還是有bug。這篇文章總結(jié)的經(jīng)驗(yàn)教訓(xùn)幫助我在編碼、測(cè)試和調(diào)試這三個(gè)階段盡量減小破壞。
評(píng)論
查看更多