天天看點

防禦性編碼和單元測試規則

開發人員編寫代碼。不幸的是,開發人員也編寫缺陷,其中大多數缺陷是在最初的編碼階段加入的。修複這些缺陷成本最低的地方同樣也是在開發的初始階段。如果等到功能測試或者系統測試來捕獲并修複缺陷,那麼您的軟體開發成本就會高得多。在本文中,作者 Scott Will、Ted Rivera 和 Adam Tate 讨論了一些基本的“防禦性”編碼和單元測試實踐,讓開發人員更容易找到缺陷 —— 更重要的是,從一開始預防缺陷産生。 

防禦性駕駛和防禦性開發 

大多數司機接受過防禦性駕駛技術的教育 —— 這有很好的理由 —— 但是并不是所有開發人員都接受過防禦性開發的教育,特别是那些沒有用彙編語言進行過多少開發(如果不是完全沒用過的話)、也沒有因記憶體限制和處理器限制而關心過編寫極其緊湊的代碼的年輕開發人員。本文讨論防禦性編碼和單元測試概念,它們可以幫助開發人員更快生成更好的代碼并且缺陷更少。 

為什麼防禦性開發是重要的? 

捕捉錯誤、問題和缺陷的最佳位置是在開發周期的早期。圖 1 展示了最容易出現缺陷的地方,以及最容易發現它們的地方,并包括了修複這些缺陷的成本(這些成本是針對 1996 年的 —— 今天的成本顯然更高)。 

<a href="http://tech.ccidnet.com/pub/attachment/2004/3/280202.gif" target="_blank"></a>

圖 1. 缺陷:引入階段及發現階段(包括成本) 

當然,比在編碼階段找到缺陷更好的是在一開始就防止它們。防止缺陷應該是開發人員最優先考慮的。我們将分析幾個讓開發人員可以在編碼和單元測試時防止并檢測缺陷的簡單的、經過證明的方法。 

在編譯前(防禦性設計考慮) 

防止缺陷(特别是系統性缺陷)的最有效方式是仔細檢查編碼所依據的設計。由設計缺陷導緻的缺陷 —— 雖然一般不是很多 —— 通常修補成本是最高的。事前花很少的時間針對以下幾點對設計進行檢查可以得到顯著的長期回報。 

設計考慮 

設計是否有任何不清楚或者混亂的部分?如果是的話,在編寫任何代碼 之前 澄清這些問題。否則,您可能以一種方式解釋一個設計需求,而同僚則以另一種方式解釋它,進而得到不相容的實作。 

如果您的代碼要通路同時被其他元件通路的資料,那麼保證您的設計可以處理這種情況。同時,檢查設計的安全問題。 

如果您的代碼嚴重依賴于其他應用程式的代碼,那麼您對那個應用程式是否熟悉到可以對設計進行檢查?考慮在您的設計檢查小組中加入熟悉該産品的一個開發人員。在 設計階段 發現的內建問題可以得到最有效的處理。 

安裝和使用考慮 

如果您的代碼是以前版本的一個更新,那麼是否有會使更新失敗的參數或者其他選項改變?有哪些其他産品與新代碼互動或者內建 —— 如果這些産品本身也改變了呢?還有,您的代碼是否容易安裝? 

作業系統和資料庫考慮 

您的代碼是否會在新版本的作業系統或者資料庫上運作?您是否知道這些新版本中加入了哪些改變以及它們是否(及如何)影響您的代碼? 

這隻是測試 

設計是否結合了可測試性?雖然您可能認為可測試性問題不是您需要關心的,但是事實上單元測試 是 開發人員的責任之一 —— 幾乎所有使執行功能測試和/或系統測試更容易的任何事情也會使單元測試更容易執行。 

下面是 可測試性 領域内容的幾個例子。 

設計是否允許運作時外部工具通路“狀态”變量(例如,目前狀态),特别是那些測試程式需要用來驗證代碼是否正确工作以幫助确定問題的變量? 

是否對跟蹤和日志給予了足夠的重視?您讓其他人分析缺陷越容易,您在發現缺陷後修正它們就越容易(而且在單元測試中發現自己的問題也會更容易)。 

您是否考慮了所有可能調用您的代碼的上下文?如果您可以将錯誤消息與調用它的使用者函數上下文相關聯,那麼使用者就更有可能了解這個錯誤。 

設計是否結合了您的測試自動化工具所需要的特定的“鈎子(hook)”? 

再多想一想您肯定可以在這個清單中加入更多的内容,特别是那些對您的産品或者組織特定的内容。 

防禦性編碼技術:編譯器是您的朋友 

當您完成對設計的檢查後,就輪到編碼了。就讓我們面對它,除了設計錯誤外,編碼是惟一引入缺陷的地方。無論如何,您的測試程式和客戶是不會加入缺陷的 —— 隻有 您 會。我們都知道時間很緊張,但是如果您沒有時間在第一次就把它編寫正确,那麼您怎麼能找到時間去修正它呢?花上一些時間,這會使您在以後的編碼工作中更輕松。 

防止缺陷的最好方法之一是使用編譯器。令人恐懼的是,開發人員在編譯時通常選擇使用最低程度的警告輸出,是以請啟用編譯的全部警告 —— 把即使将編譯器配置為檢查 所有方面 編譯時也不産生一個警告當成編寫代碼的一個挑戰。此外,對代碼使用多種編譯器使很多程式員獲益 —— 這種方法有時會捕獲不同的文法錯誤。 

編碼習慣 

下面我們将抛磚引玉介紹幾個好的編碼習慣。我們不是要為您定義“最佳編碼習慣” —— 我們隻是要您形成自己遵守的代碼編寫習慣。下面是幾個供參考的最佳習慣的例子。 

在使用前初始化所有變量 

您是否有一組可接受的預設值,特别是對于可能被使用者、其他元件或者其他程式有選擇地修改的資料?同時,我們強烈要求您列出在最外圍例程中要使用的所有本地變量,然後再專門初始化它們。這樣不會對您編寫代碼時的想法留下任何疑問。雖然這可能要多花一些時間并且像是沒有理由地增加了幾行代碼,但是與隻是在“運作中(on the fly)”聲明本地變量相比,大多數優化編譯器不會對此生成任何額外的運作時代碼。清單 1 顯示了在一個例程中最初幾行代碼的一個例子: 

清單 1. 初始化本地變量 

public unsigned short TransMogrify( UFEventLink IncomingLink )

  {

    //

    // local variables

    unsigned short usRc;

    String sOurEventType;

    String sTheirEventType;

    // beginning of code

    usRc = 0;

    sOurEventType = null;

    sTheirEventType = null;

    // a miracle occurs...

    return( usRc );

  } // end "TransMogrify"

使用一個“編碼标準”文檔 

如果您有一個編碼标準文檔,就使用它。您可以在 Internet 上找到許多種編碼标準。找到一種簡單的、切中要害、并為您留下一定的活動空間的标準。Sun 的網站有一個關于 Java 程式設計的編碼規範的文章(請參閱 參考資料),它給出了擁有标準的下列幾點理由: 

一個軟體生存期百分之八十的成本都用在維護上。 

幾乎沒有軟體在整個使用期間都是由原作者維護的。 

編碼規範改進了軟體的可讀性,使工程師可以更快和更充分地了解新代碼。 

如果您将源代碼作為産品傳遞,那麼需要保證它有像您建立的所有其他産品一樣的包裝和整潔性。 

即使不贊成“标準”的想法,至少采用這個簡單的建議:對變量名使用“匈牙利命名法”,這會使您的代碼更容易閱讀和維護(。 

保證傳回代碼的一緻性 

在調試時有一種會制造麻煩的情況是:調用程式屏蔽(或者覆寫)一個表示錯誤的傳回代碼。一定要想好您要向調用您的代碼的例程傳回什麼值,并保證從您所調用的例程傳回的所有錯誤代碼都得到恰當處理。如果傳回代碼 n 在一個地方意味着一件事,就不要在其他的地方用傳回代碼 n 表示另一件事。 

對每個例程使用“單點退出” 

這一點怎麼強調也不過分:對每個例程使用單點退出 —— 就是說,沒有多重傳回!這是最容易忽視的、也是您可以采用的最好的習慣。如果例程隻從一個地方傳回,那麼就可以用一種非常容易的方法保證在傳回前完成所有必要的清理工作,這也使調試更容易。清單 2 顯示了一個包含多重傳回的代碼示例。注意重複代碼、甚至忘記“清理”項目是多麼容易。 

清單 2. 單點退出示例 

1   public String getName( )

2     {

3       //

4       // local variables

5       //

6       String returnString;

7

8

9       //

10      // beginning of code

11      //

12      returnString = textField.getText( );

13      if ( null == returnstring )

14        {

15          badCount++;

16          totalCount++;

17          return( null )

18        }

19

20      returnString = returnString.trim( );

21      if ( returnString.equals( "" ) )

22        {

23          badCount++;

24          totalCount++;

25          return( null );

26        }

27

28      totalCount++;

29      return( returnString );

30

31    } // end getName

在第 15 行,badCount 增加了,因為 getText( ) 傳回 null。在第 23 行,badCount 代碼又重複了。現在想像一下如果這個例子需要完成更複雜的“清理”時會有多混亂。 

清單 3 顯示了一種更好的方法: 

清單 3. 單點退出示例 —— 修正後 

13      if ( null != returnstring )

15          returnString = returnString.trim( );

16          if ( returnString.equals( "" ) )

17            returnString = null;

20      //

21      // "cleanup"

22      //

23      if ( null == returnString )

24        badCount++;

25      totalCount++;

26

27      return( returnString );

28

29    } // end getName

标題   防禦性編碼和單元測試規則(二)      chensheng913 [原作]  

關鍵字   防禦性編碼和單元測試規則(二)   出處     加強警戒(En garde)! 要記住,您的客戶對您的産品有與您不一樣的想法。他們會在一個您的小組很可能從來也沒想到的 —— 或者至少是沒有可能測試的 —— 環境中安裝它。他們會以您從來沒有想到過的方法使用它,并以您意想不到的方法配置它。下面的清單有助于幫助您保證他們不會發怒: 驗證所有收到的參數的完整性(考慮如果您期待一個數組而傳遞來的是一個 null,但是您在索引數組之前沒有檢查這種可能性時會發生什麼情況)。 考慮所有可能的錯誤情況并增加處理每種情況的代碼(您希望代碼得體地處理錯誤條件而不是堵塞它)。 對于那些未預料到的錯誤條件,加入一個一般性的“捕獲所有”錯誤處理程式。 在适當的時候和地點使用常量。 在代碼各處加入跟蹤和日志。 如果您的産品将翻譯為另一種語言,那麼保證您的代碼可以“支援”它。即使出現這種情況的機會很小,但是提前計劃總是好一些。修改代碼以使它提供支援是最容易産生缺陷的。下面是幾個您要考慮的與支援相關的問題: 您是否有任何寫死的字元串? 您是否正确地處理不同的日期/時間? 不同的貨币表示呢? 還有,在代碼中使用大量斷言。 給您的代碼加上充分的 注釋。總之,您還記得在六個月前編寫那個方法時的想法嗎?一年後要修改您的代碼的某個人又會怎麼想呢?在我們提出的所有建議中,這一條可能是最重要的。 單元測試(防禦性測試技術) 在本文中,我們所說的 單元測試 是開發人員在自己的代碼正确編譯後、在交給功能測試小組之前進行的所有測試和分析。正如我們在 這隻是一個測試 中提到的,主動進行單元測試并 在測試時像一位測試者那樣思考(即,必須往壞處想、熱衷于破壞并喜歡惡作劇)是很重要的。下面是在單元測試時要記住的幾件事。 靜态代碼分析工具 第一種,也是最容易的分析代碼的方法是讓别人替您做 —— 或者像在這裡一樣,讓其他 工具 替您做。有一些不同的靜态代碼分析工具可用,從綜合性的工具 —— 一些開發機構實際上在他們的“編譯”環境(這可是需要購買的)中加入了這樣的工具 —— 到其他可以免費從 Internet 上下載下傳的工具。 發現缺陷 當您準備運作代碼并檢查缺陷時,要記住往壞處想。這些缺陷是您所建立的或者由您忽略的代碼産生。下面是一些幫助您找到代碼中缺陷的提示: 試着強行制造您所想到的所有錯誤條件并檢查可以出現的所有錯誤消息。 試着使用與其他元件或者程式互動的代碼路徑。如果其他程式或者元件還不存在,那麼就自己編寫一些腳手架代碼以使您可以試用 API 或者填充共享記憶體、共享隊列,等等。并讓您的功能測試小組可以使用這個腳手架代碼,這樣他們就可以把它加入到他們的武器庫中。 對于 GUI 中的每一個輸入字段,試驗下面多種不同的組合(考慮 自動化): 不可接受的字元(控制字元、非列印字元等)。 過多的字元。 過少的字元。 負數(特别是當您隻期待正數時)。 過大和/或者過小的數。 剪切和粘貼資料和文本到輸入字段,特别是當您編寫的代碼限制使用者可以鍵入該字段的内容時。 文本和數字的組合。 全大寫字母和全小寫字母。 為代碼建立“壓力條件”,如大量活動、慢連接配接的網絡和所有您想到的可以将代碼推到極限條件的東西。 反複進行同樣的步驟,然後: 檢查未預計到的記憶體損失條件。 檢查當記憶體用光時發生什麼。 試圖建立緩存溢出、滿隊列、不可用的緩存以及其他“不能正确工作”的情況。 對于數組和緩存,試着向數組(或者緩存)增加 n 個資料項,然後試圖删除 n+1 個項。 關于時間的考慮? 如果操作“b”在操作“a”之前發生會怎麼樣?就算您 認為 它不會發生 —— 您能 使 它發生嗎?如果是的話,可以打賭您的客戶會使它發生的。最好現在找出來,而不是在修複成本更高、并聽到客戶報怨您的軟體品質糟糕之後再去做。 腳手架代碼 我們在前面 發現缺陷 中讨論了腳手架代碼。如果是為自己的使用需要而建立的,一定要将它交給驗證工程師。可能您提供的腳手架代碼使他們可以很快地開始測試您的代碼,或者至少使他們更好地了解當其他元件存在時可以預期什麼。 如果您的産品有保護性的安全功能,那麼您必須測試它們。提供可以建立您想要防止的情況的腳手架代碼是很重要的:您必須能夠建立系統試圖防止的那種情況。 腳手架代碼的另一個簡單例子是提供操縱隊列的代碼。如果您的産品使用隊列,那麼想像如果有一個可以在運作時從隊列中增加或者删除項、破壞隊列中的資料(以保證适當的錯誤處理)等等的工具會有多友善。 源代碼級調試程式 使用源代碼級的調試程式是進行徹底和成功的單元測試的關鍵方法。開發人員應該與他們的調試程式共生死。不幸的是,對源代碼級的調試程式的充分了解和使用是一種正在消亡的做法,盡管這些調試程式的好處遠遠超過任何學習曲線。簡而言之,我們強烈鼓勵您全面學習一種調試程式。下面是用源代碼級調試程式對代碼進行單元測試的幾種方法。您可以: 在運作中操縱資料 —— 例如,在輸入代碼時設定中斷點,然後重新設定傳遞的參數值以檢查代碼是否能正确處理(現在為)無效的參數。以這種方式使用調試程式就不需要讓錯誤條件真正發生。 設定斷點,然後“單步”通過代碼,這樣您就可以看到每一行代碼所做的事情。 設定對變量的“監視(watch)”。 強制使用錯誤代碼路徑。 觀察調用堆棧以檢查哪一個例程調用了您的代碼。 在錯誤發生時“捕獲”它們。 執行邊界檢查。 認識您的驗證工程師 驗證工程師是測試知識的很好來源。他們可以給您指出要測試什麼并幫助您了解可以在代碼中加入什麼以幫助他們測試(如代碼鈎子)。此外,您可以向他們展示如何使用您的腳手架代碼。他們還會很有興趣了解您認為在測試中哪些應該是自動化的 —— 如果您某些事情做了不止一遍,那麼他們也會。 開始測驗! 現在是進行小測試的時候了。讓我們看看您是否用心了。 問題 您希望檢查一個整數的值是否為 5。通常,要這樣編寫代碼: if ( i == 5 ) then   {     //     // do something...     //   } 不過,如果您對代碼進行“手指檢查”,并且把代碼寫成了下面這樣會出現什麼情況呢? if ( i = 5 ) then   {     //     // do something...     //   } 這個失誤是一個缺陷,但是隻有在運作時才能捕獲它 —— 可能需要相當的調試努力才能找到它。編譯器會輕易放過您的代碼,那麼如何防止這種錯誤發生? 答案 實際上有兩個答案:您可以使用一種上面描述的靜态代碼分析工具,并希望它有足夠的健壯性可以捕獲這種錯誤,也可以交換操作數以使常量位于左邊: if ( 5 == i ) then   {     //     // do something...     //   } 因為這種方法保證您可以在編譯代碼時立即捕捉到問題,是以它是首選的技術。雖然它看上去有些笨,但是代碼可以編譯并運作得很好。然而,當您“手指檢查”代碼時就可以立即看到好處了: if ( 5 = i ) then   {     //     // do something...     //   } 可是編譯器不喜歡這樣,因為 5 不能被指派為另一個值。這就是我們在 前面 說您應當将編譯器看成是您的朋友時所表達的意思。 您還可以在檢查 null 指針時使用這個技巧。看下面的代碼: if ( returnString == null )   {     //     // do something...     //   } 如果您偶然将它“誤寫”成下面這樣會發生什麼呢? if ( returnString = null )   {     //     // do something...     //   } 您可能不會得到想要的結果。而改用下面的方法您會得到與我們剛描述過的同樣的“編譯器保護”: if ( null == returnString )   {     //     // do something...     //   } 結束語 為保持簡明扼要我們做了一個相當簡潔的歸納:要麼現在去做,要麼以後花 多得多 的代價去做。換句話說,您在開發周期的早期在測試和預防代碼缺陷上花的時間越多,您在以後節省的時間和金錢就越多。這就是防禦性編碼的意義。它就是這麼簡單。