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