我一直在如何跟蹤遇到的最有意思的bug。最近回顧了全部194個bug(時間跨度達13年),看看從中學到了什么經驗教訓。下面是最重要的幾個經驗教訓,分為編碼、測試和調試這三大類:
編碼
這些是在過去給我帶來棘手bug的所有問題:
1. 事件順序:處理事件時,很有必要提出下列問題:事件是否可以以一種不同的順序到達?如果我們從來沒有收到該事件,會怎樣?如果該事件連續出現兩次,又會怎樣?即使通常情況下這永遠不會出現,但系統(或交互系統)的其他部分中的bug可能會導致這出現。
2. 處理太早:這是上述“事件順序”的一種特殊情況,不過它已引起了一些棘手的bug,所以它自成一類。比如說,如果信令消息接收太早,在配置和啟動過程完成之前接收,許多奇怪的行為就會出現。另一個例子:當某個網絡連接還沒有被列入空閑列表就被標為斷開。調試這個問題時,我們總是假設它在處于空閑列表時被設為斷開(但為什么它又沒有從列表上撤下?)。沒考慮到有時動作發生太早要怪我們沒想到。
3. 隱蔽故障:一些跟蹤起來最棘手的bug(一方面)是由出現隱蔽故障、繼續執行而不是給出錯誤的代碼引起的。比如說,系統調用(比如綁定)返回未加檢查的錯誤代碼。另一個例子:遇到錯誤元素后,直接返回而不是給出錯誤的解析代碼。調用在故障狀態下繼續持續一段時間,這大大加大了調試的難度。最好一旦檢測到故障情況,就返回錯誤。
4. if語句:有幾個條件的if語句給我帶來了許多bug。即使if語句概念上很簡單,有多個條件需要跟蹤時,它們也很容易搞錯。如今我試著重寫代碼,力求更簡單,避免要處理復雜的if語句。
5. Else:有幾個bug是沒有適當考慮如果條件為假會發生什么而引起的。幾乎無一例外的是,每個if語句應該有一個else部分。此外,如果你在if語句的一個分支中設置了某個變量,可能應該在另一個分支也要設置該變量。與此相關的是標志(flag)被設定的情況。僅僅添加設定標志的條件很容易,但是容易忘了添加應該重新設定標志的條件。任由永久性設定的標志留在那里可能會在將來導致bug。
6. 不斷變化的假設:一開始最難預防的許多bug是由不斷變化的假設引起的。比如說,一開始,可能每天只有一個客戶事件。然后,按照這種假設編寫了許多代碼。后來某個時候,設計發生了變化,允許每天有多個客戶事件。出現這種情況后,就很難改變受到新設計影響的所有情況。很容易找到顯式依賴該變化的所有項,但是難就難在,找到隱式依賴舊設計的所有情況。比如說,可能有代碼讀取某一天的所有客戶事件。隱式的假設可能是,結果集從不大于客戶數量。我沒有好的辦法可以預防這類問題,歡迎讀者建議。
7. 日志:深入了解程序執行的任務至關重要,尤其是邏輯很復雜時。務必要添加足夠多(但是別太多)的日志,那樣你就能弄清楚為什么程序在執行它執行的任務。如果一切正常,日志并不重要,但是一旦出現了問題(這不可避免),你會很高興添加了適當的日志記錄。、
測試
作為一名開發者,除非進行了測試,否則我不會說搞完了一項功能。至少,這意味著每一行新代碼或更改后的代碼至少執行了一次。此外,單元測試或功能測試也很好,但還不夠。新功能還必須在類似生產環境的環境下加以測試和探究。下面是bug在測試方面給予我的一些重要的經驗教訓:
8. 零和空:務必要以零和空(合適的情況下)來進行測試。對于字符串而言,這意味著既指長度為零的字符串,又指內容為空的字符串。另一個例子:在發送任何數據(零字節)之前,測試TCP連接的斷開。沒有使用這些組合來測試是bug悄然出現的頭號原因,我在測試時是原本可以發現這些bug的。
9. 添加和刪除:新功能常常需要能夠為系統添加新配置,比如說用于電話號碼翻譯的新配置文件。所以測試它切實可行、以便添加新的配置文件很自然不過。然而,我發現很容易忘了還要測試配置文件的刪除。
10. 錯誤處理:處理錯誤的代碼常常很難測試。最好由自動測試來檢查錯誤處理代碼,但有時這不可能。這種情況下,我有時采用的一招就是,臨時修改代碼,讓錯誤處理代碼運行。要做到這一點,最容易的方法就是反轉if語句,比如說將if語句由error_count > 0反轉為error_count == 0。另一個例子是誤拼數據庫列名,讓所需的錯誤處理代碼運行。
11. 隨機性輸入:常常可以發現bug的一種測試方法就是使用隨機性輸入。比如說,H.323協議的ASN.1解碼可處理二進制數據。通過發送有待解碼的隨機性字節,我們發現了解碼器中的幾個bug。另一個例子是使用測試調用生成腳本,其中調用持續時間、回復延遲、第一方掛斷等都是隨機生成的內容。這些測試腳本暴露了無數bug,尤其是接踵而至的事件引起的干擾。
12. 檢查什么不該發生:測試常常包括檢查所需的動作已發生。但它很容易忽視相反的情況――檢查不該發生的動作確實沒有發生。
13. 自行編寫工具:我通常構建自己的小工具,好讓測試更容易。比如說,我在處理面向VoIP的SIP協議時,寫了一個小腳本,就返回我所需要的頭和值。有了這個工具,許多個別情況測試起來很容易。另一個例子是可以進行API調用的命令行工具。通過從小處著手,然后根據需要逐步添加功能,我最后開發出了非常實用的工具。自行編寫工具的好處就是,我獲得了所需的那種功能。
不過根本不可能在測試中發現所有bug,有一回,我改變了由兩部分組成的處理關聯號碼的機制:路由地址前綴(始終一樣),以及從000到999的動態分配號碼。問題是,查找關聯時,動態分配號碼的第一位數字在查詢地址表之前就被誤刪除了。所以,不是尋找637之類的號碼,你尋找的是37,而這個號碼不在表中。這意味著,它一直尋找到100,所以前100個調用正常,而之余的所有900個調用失效。所以除非我在重新啟動之前測試了100多次,否則在測試時發現不了這個問題。
調試
14. 討論:在過去對我幫助最大的調試方法就是與同事討論問題。我常常只要向同事描述問題,就足以認識到問題是什么。此外,即使同事不是很熟悉相應代碼,常常也能給出好主意,表明哪里可能有問題。我在處理最棘手的bug時,與同事討論這一招來得尤其管用。
15. 密切關注:調試某個問題花很長時間時,常常是由于我做了錯誤的假設。比如說,我以為問題出現在某個方法中,而實際上這個問題根本不會出現在這個方法中。或者拋出的異常并不是我假設的那個異常。或者我以為在運行軟件的最新版本,實際上運行的是舊版本。因此,一定要核實這些細節,而不是犯想當然的毛病。很容易看見預期看見的問題,而不是實際擺在那里的問題。
16. 最近的變化:過去可以運行的代碼現在無法運行時,這常常是最后一個變更的對象引起的。有一回,最近變化的對象只是日志,但是日志中的錯誤引起了更大的問題。為了讓諸如此類的回歸更容易找到,有必要在不同的提交代碼中實行不同的變更,并且要清楚說明變更。
17. 相信用戶:有時候用戶報告問題時,我的本能反應是“這不可能。他們肯定是哪里弄錯了。”但是我已學會了擯棄這樣的反應。結果往往證明,用戶報告的正是實際發生的問題。所以如今,我對用戶報告的問題信以為真。當然,我仍反復核查各方面已正確設定。但是我碰過好多情況下,之所以發生奇怪的問題,是由于不同尋常的配置或意料之外的使用,而我默認的假設是,它們是正確的,程序是錯誤的。
18. 測試修正版:bug的修正版準備就緒后,它必須進行測試。先在沒有修正版的情況下運行代碼,觀察bug。然后打上修正版,重復測試用例。現在,錯誤行為應該消失。遵照這些步驟可以確保它其實是個bug,確保修正版確實解決了問題。這很簡單,又必不可少。
其他意見
這13年來我一直在跟蹤我遇到的最棘手的bug,這期間發生了很大的變化。我開發過一個小型嵌入式系統、一個大型電信系統以及一個基于Web的系統。我用C 、Ruby、Java和Python編寫過代碼。我用C 編碼時期的幾類bug已完全消失,比如堆棧溢出、內存損壞、字符串問題以及某些形式的內存泄漏。
我遇到的其他問題(比如循環錯誤和個別情況)少了很多,那是由于我一直對更多的邏輯進行單元測試。但是,這并不意味著沒有bug,還是有bug。這篇文章總結的經驗教訓幫助我在編碼、測試和調試這三個階段盡量減小破壞。
-
編碼
+關注
關注
6文章
945瀏覽量
54854 -
BUG
+關注
關注
0文章
155瀏覽量
15679
原文標題:回顧了快200個bug,這個老工程師總結這些教訓
文章出處:【微信號:edn-china,微信公眾號:EDN電子技術設計】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論