標(biāo)題?? 防御性編碼和單元測試規(guī)則(一)??????chensheng913 [原作]??
關(guān)鍵字?? 防御性編碼和單元測試規(guī)則(一)??
出處????
開發(fā)人員編寫代碼。不幸的是,開發(fā)人員也編寫缺陷,其中大多數(shù)缺陷是在最初的編碼階段加入的。修復(fù)這些缺陷成本最低的地方同樣也是在開發(fā)的初始階段。如果等到功能測試或者系統(tǒng)測試來捕獲并修復(fù)缺陷,那么您的軟件開發(fā)成本就會高得多。在本文中,作者 Scott Will、Ted Rivera 和 Adam Tate 討論了一些基本的“防御性”編碼和單元測試實(shí)踐,讓開發(fā)人員更容易找到缺陷 —— 更重要的是,從一開始預(yù)防缺陷產(chǎn)生。
防御性駕駛和防御性開發(fā)
大多數(shù)司機(jī)接受過防御性駕駛技術(shù)的教育 —— 這有很好的理由 —— 但是并不是所有開發(fā)人員都接受過防御性開發(fā)的教育,特別是那些沒有用匯編語言進(jìn)行過多少開發(fā)(如果不是完全沒用過的話)、也沒有因內(nèi)存約束和處理器限制而關(guān)心過編寫極其緊湊的代碼的年輕開發(fā)人員。本文討論防御性編碼和單元測試概念,它們可以幫助開發(fā)人員更快生成更好的代碼并且缺陷更少。
為什么防御性開發(fā)是重要的?
捕捉錯誤、問題和缺陷的最佳位置是在開發(fā)周期的早期。圖 1 展示了最容易出現(xiàn)缺陷的地方,以及最容易發(fā)現(xiàn)它們的地方,并包括了修復(fù)這些缺陷的成本(這些成本是針對 1996 年的 —— 今天的成本顯然更高)。

圖 1. 缺陷:引入階段及發(fā)現(xiàn)階段(包括成本)
當(dāng)然,比在編碼階段找到缺陷更好的是在一開始就防止它們。防止缺陷應(yīng)該是開發(fā)人員最優(yōu)先考慮的。我們將分析幾個讓開發(fā)人員可以在編碼和單元測試時防止并檢測缺陷的簡單的、經(jīng)過證明的方法。
在編譯前(防御性設(shè)計考慮)
防止缺陷(特別是系統(tǒng)性缺陷)的最有效方式是仔細(xì)檢查編碼所依據(jù)的設(shè)計。由設(shè)計缺陷導(dǎo)致的缺陷 —— 雖然一般不是很多 —— 通常修補(bǔ)成本是最高的。事前花很少的時間針對以下幾點(diǎn)對設(shè)計進(jìn)行檢查可以得到顯著的長期回報。
設(shè)計考慮
設(shè)計是否有任何不清楚或者混亂的部分?如果是的話,在編寫任何代碼 之前 澄清這些問題。否則,您可能以一種方式解釋一個設(shè)計需求,而同事則以另一種方式解釋它,從而得到不兼容的實(shí)現(xiàn)。
如果您的代碼要訪問同時被其他組件訪問的數(shù)據(jù),那么保證您的設(shè)計可以處理這種情況。同時,檢查設(shè)計的安全問題。
如果您的代碼嚴(yán)重依賴于其他應(yīng)用程序的代碼,那么您對那個應(yīng)用程序是否熟悉到可以對設(shè)計進(jìn)行檢查?考慮在您的設(shè)計檢查小組中加入熟悉該產(chǎn)品的一個開發(fā)人員。在 設(shè)計階段 發(fā)現(xiàn)的集成問題可以得到最有效的處理。
安裝和使用考慮
如果您的代碼是以前版本的一個升級,那么是否有會使升級失敗的參數(shù)或者其他選項(xiàng)改變?有哪些其他產(chǎn)品與新代碼交互或者集成 —— 如果這些產(chǎn)品本身也改變了呢?還有,您的代碼是否容易安裝?
操作系統(tǒng)和數(shù)據(jù)庫考慮
您的代碼是否會在新版本的操作系統(tǒng)或者數(shù)據(jù)庫上運(yùn)行?您是否知道這些新版本中加入了哪些改變以及它們是否(及如何)影響您的代碼?
這只是測試
設(shè)計是否結(jié)合了可測試性?雖然您可能認(rèn)為可測試性問題不是您需要關(guān)心的,但是事實(shí)上單元測試 是 開發(fā)人員的責(zé)任之一 —— 幾乎所有使執(zhí)行功能測試和/或系統(tǒng)測試更容易的任何事情也會使單元測試更容易執(zhí)行。
下面是 可測試性 領(lǐng)域內(nèi)容的幾個例子。
設(shè)計是否允許運(yùn)行時外部工具訪問“狀態(tài)”變量(例如,當(dāng)前狀態(tài)),特別是那些測試程序需要用來驗(yàn)證代碼是否正確工作以幫助確定問題的變量?
是否對跟蹤和日志給予了足夠的重視?您讓其他人分析缺陷越容易,您在發(fā)現(xiàn)缺陷后修正它們就越容易(而且在單元測試中發(fā)現(xiàn)自己的問題也會更容易)。
您是否考慮了所有可能調(diào)用您的代碼的上下文?如果您可以將錯誤消息與調(diào)用它的用戶函數(shù)上下文相關(guān)聯(lián),那么用戶就更有可能理解這個錯誤。
設(shè)計是否結(jié)合了您的測試自動化工具所需要的特定的“鉤子(hook)”?
再多想一想您肯定可以在這個清單中加入更多的內(nèi)容,特別是那些對您的產(chǎn)品或者組織特定的內(nèi)容。
防御性編碼技術(shù):編譯器是您的朋友
當(dāng)您完成對設(shè)計的檢查后,就輪到編碼了。就讓我們面對它,除了設(shè)計錯誤外,編碼是惟一引入缺陷的地方。無論如何,您的測試程序和客戶是不會加入缺陷的 —— 只有 您 會。我們都知道時間很緊張,但是如果您沒有時間在第一次就把它編寫正確,那么您怎么能找到時間去修正它呢?花上一些時間,這會使您在以后的編碼工作中更輕松。
防止缺陷的最好方法之一是使用編譯器。令人恐懼的是,開發(fā)人員在編譯時通常選擇使用最低程度的警告輸出,所以請啟用編譯的全部警告 —— 把即使將編譯器配置為檢查 所有方面 編譯時也不產(chǎn)生一個警告當(dāng)成編寫代碼的一個挑戰(zhàn)。此外,對代碼使用多種編譯器使很多程序員獲益 —— 這種方法有時會捕獲不同的語法錯誤。
編碼習(xí)慣
下面我們將拋磚引玉介紹幾個好的編碼習(xí)慣。我們不是要為您定義“最佳編碼習(xí)慣” —— 我們只是要您形成自己遵守的代碼編寫習(xí)慣。下面是幾個供參考的最佳習(xí)慣的例子。
在使用前初始化所有變量
您是否有一組可接受的默認(rèn)值,特別是對于可能被用戶、其他組件或者其他程序有選擇地修改的數(shù)據(jù)?同時,我們強(qiáng)烈要求您列出在最外圍例程中要使用的所有本地變量,然后再專門初始化它們。這樣不會對您編寫代碼時的想法留下任何疑問。雖然這可能要多花一些時間并且像是沒有理由地增加了幾行代碼,但是與只是在“運(yùn)行中(on the fly)”聲明本地變量相比,大多數(shù)優(yōu)化編譯器不會對此生成任何額外的運(yùn)行時代碼。清單 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"
使用一個“編碼標(biāo)準(zhǔn)”文檔
如果您有一個編碼標(biāo)準(zhǔn)文檔,就使用它。您可以在 Internet 上找到許多種編碼標(biāo)準(zhǔn)。找到一種簡單的、切中要害、并為您留下一定的活動空間的標(biāo)準(zhǔn)。Sun 的網(wǎng)站有一個關(guān)于 Java 編程的編碼規(guī)范的文章(請參閱 參考資料),它給出了擁有標(biāo)準(zhǔn)的下列幾點(diǎn)理由:
一個軟件生存期百分之八十的成本都用在維護(hù)上。
幾乎沒有軟件在整個使用期間都是由原作者維護(hù)的。
編碼規(guī)范改進(jìn)了軟件的可讀性,使工程師可以更快和更充分地理解新代碼。
如果您將源代碼作為產(chǎn)品交付,那么需要保證它有像您創(chuàng)建的所有其他產(chǎn)品一樣的包裝和整潔性。
即使不贊成“標(biāo)準(zhǔn)”的想法,至少采用這個簡單的建議:對變量名使用“匈牙利命名法”,這會使您的代碼更容易閱讀和維護(hù)(。
保證返回代碼的一致性
在調(diào)試時有一種會制造麻煩的情況是:調(diào)用程序屏蔽(或者覆蓋)一個表示錯誤的返回代碼。一定要想好您要向調(diào)用您的代碼的例程返回什么值,并保證從您所調(diào)用的例程返回的所有錯誤代碼都得到恰當(dāng)處理。如果返回代碼 n 在一個地方意味著一件事,就不要在其他的地方用返回代碼 n 表示另一件事。
對每個例程使用“單點(diǎn)退出”
這一點(diǎn)怎么強(qiáng)調(diào)也不過分:對每個例程使用單點(diǎn)退出 —— 就是說,沒有多重返回!這是最容易忽視的、也是您可以采用的最好的習(xí)慣。如果例程只從一個地方返回,那么就可以用一種非常容易的方法保證在返回前完成所有必要的清理工作,這也使調(diào)試更容易。清單 2 顯示了一個包含多重返回的代碼示例。注意重復(fù)代碼、甚至忘記“清理”項(xiàng)目是多么容易。
清單 2. 單點(diǎn)退出示例
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 增加了,因?yàn)?getText( ) 返回 null。在第 23 行,badCount 代碼又重復(fù)了。現(xiàn)在想像一下如果這個例子需要完成更復(fù)雜的“清理”時會有多混亂。
清單 3 顯示了一種更好的方法:
清單 3. 單點(diǎn)退出示例 —— 修正后
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??????????returnString = returnString.trim( );
16??????????if ( returnString.equals( "" ) )
17????????????returnString = null;
18????????}
19
20??????//
21??????// "cleanup"
22??????//
23??????if ( null == returnString )
24????????badCount++;
25??????totalCount++;
26
27??????return( returnString );
28
29????} // end getName
標(biāo)題?? 防御性編碼和單元測試規(guī)則(二)??????chensheng913 [原作]??
關(guān)鍵字?? 防御性編碼和單元測試規(guī)則(二)?? 出處???? 加強(qiáng)警戒(En garde)! 要記住,您的客戶對您的產(chǎn)品有與您不一樣的想法。他們會在一個您的小組很可能從來也沒想到的 —— 或者至少是沒有可能測試的 —— 環(huán)境中安裝它。他們會以您從來沒有想到過的方法使用它,并以您意想不到的方法配置它。下面的列表有助于幫助您保證他們不會發(fā)怒: 驗(yàn)證所有收到的參數(shù)的完整性(考慮如果您期待一個數(shù)組而傳遞來的是一個 null,但是您在索引數(shù)組之前沒有檢查這種可能性時會發(fā)生什么情況)。 考慮所有可能的錯誤情況并增加處理每種情況的代碼(您希望代碼得體地處理錯誤條件而不是堵塞它)。 對于那些未預(yù)料到的錯誤條件,加入一個一般性的“捕獲所有”錯誤處理程序。 在適當(dāng)?shù)臅r候和地點(diǎn)使用常量。 在代碼各處加入跟蹤和日志。 如果您的產(chǎn)品將翻譯為另一種語言,那么保證您的代碼可以“支持”它。即使出現(xiàn)這種情況的機(jī)會很小,但是提前計劃總是好一些。修改代碼以使它提供支持是最容易產(chǎn)生缺陷的。下面是幾個您要考慮的與支持相關(guān)的問題: 您是否有任何硬編碼的字符串? 您是否正確地處理不同的日期/時間? 不同的貨幣表示呢? 還有,在代碼中使用大量斷言。 給您的代碼加上充分的 注釋??傊?,您還記得在六個月前編寫那個方法時的想法嗎?一年后要修改您的代碼的某個人又會怎么想呢?在我們提出的所有建議中,這一條可能是最重要的。 單元測試(防御性測試技術(shù)) 在本文中,我們所說的 單元測試 是開發(fā)人員在自己的代碼正確編譯后、在交給功能測試小組之前進(jìn)行的所有測試和分析。正如我們在 這只是一個測試 中提到的,主動進(jìn)行單元測試并 在測試時像一位測試者那樣思考(即,必須往壞處想、熱衷于破壞并喜歡惡作劇)是很重要的。下面是在單元測試時要記住的幾件事。 靜態(tài)代碼分析工具 第一種,也是最容易的分析代碼的方法是讓別人替您做 —— 或者像在這里一樣,讓其他 工具 替您做。有一些不同的靜態(tài)代碼分析工具可用,從綜合性的工具 —— 一些開發(fā)機(jī)構(gòu)實(shí)際上在他們的“編譯”環(huán)境(這可是需要購買的)中加入了這樣的工具 —— 到其他可以免費(fèi)從 Internet 上下載的工具。 發(fā)現(xiàn)缺陷 當(dāng)您準(zhǔn)備運(yùn)行代碼并檢查缺陷時,要記住往壞處想。這些缺陷是您所創(chuàng)建的或者由您忽略的代碼產(chǎn)生。下面是一些幫助您找到代碼中缺陷的提示: 試著強(qiáng)行制造您所想到的所有錯誤條件并檢查可以出現(xiàn)的所有錯誤消息。 試著使用與其他組件或者程序交互的代碼路徑。如果其他程序或者組件還不存在,那么就自己編寫一些腳手架代碼以使您可以試用 API 或者填充共享內(nèi)存、共享隊(duì)列,等等。并讓您的功能測試小組可以使用這個腳手架代碼,這樣他們就可以把它加入到他們的武器庫中。 對于 GUI 中的每一個輸入字段,試驗(yàn)下面多種不同的組合(考慮 自動化): 不可接受的字符(控制字符、非打印字符等)。 過多的字符。 過少的字符。 負(fù)數(shù)(特別是當(dāng)您只期待正數(shù)時)。 過大和/或者過小的數(shù)。 剪切和粘貼數(shù)據(jù)和文本到輸入字段,特別是當(dāng)您編寫的代碼限制用戶可以鍵入該字段的內(nèi)容時。 文本和數(shù)字的組合。 全大寫字母和全小寫字母。 為代碼創(chuàng)建“壓力條件”,如大量活動、慢連接的網(wǎng)絡(luò)和所有您想到的可以將代碼推到極限條件的東西。 反復(fù)進(jìn)行同樣的步驟,然后: 檢查未預(yù)計到的內(nèi)存損失條件。 檢查當(dāng)內(nèi)存用光時發(fā)生什么。 試圖創(chuàng)建緩存溢出、滿隊(duì)列、不可用的緩存以及其他“不能正確工作”的情況。 對于數(shù)組和緩存,試著向數(shù)組(或者緩存)增加 n 個數(shù)據(jù)項(xiàng),然后試圖刪除 n+1 個項(xiàng)。 關(guān)于時間的考慮? 如果操作“b”在操作“a”之前發(fā)生會怎么樣?就算您 認(rèn)為 它不會發(fā)生 —— 您能 使 它發(fā)生嗎?如果是的話,可以打賭您的客戶會使它發(fā)生的。最好現(xiàn)在找出來,而不是在修復(fù)成本更高、并聽到客戶報怨您的軟件質(zhì)量糟糕之后再去做。 腳手架代碼 我們在前面 發(fā)現(xiàn)缺陷 中討論了腳手架代碼。如果是為自己的使用需要而創(chuàng)建的,一定要將它交給驗(yàn)證工程師。可能您提供的腳手架代碼使他們可以很快地開始測試您的代碼,或者至少使他們更好地理解當(dāng)其他組件存在時可以預(yù)期什么。 如果您的產(chǎn)品有保護(hù)性的安全功能,那么您必須測試它們。提供可以創(chuàng)建您想要防止的情況的腳手架代碼是很重要的:您必須能夠創(chuàng)建系統(tǒng)試圖防止的那種情況。 腳手架代碼的另一個簡單例子是提供操縱隊(duì)列的代碼。如果您的產(chǎn)品使用隊(duì)列,那么想像如果有一個可以在運(yùn)行時從隊(duì)列中增加或者刪除項(xiàng)、破壞隊(duì)列中的數(shù)據(jù)(以保證適當(dāng)?shù)腻e誤處理)等等的工具會有多方便。 源代碼級調(diào)試程序 使用源代碼級的調(diào)試程序是進(jìn)行徹底和成功的單元測試的關(guān)鍵方法。開發(fā)人員應(yīng)該與他們的調(diào)試程序共生死。不幸的是,對源代碼級的調(diào)試程序的充分了解和使用是一種正在消亡的做法,盡管這些調(diào)試程序的好處遠(yuǎn)遠(yuǎn)超過任何學(xué)習(xí)曲線。簡而言之,我們強(qiáng)烈鼓勵您全面學(xué)習(xí)一種調(diào)試程序。下面是用源代碼級調(diào)試程序?qū)Υa進(jìn)行單元測試的幾種方法。您可以: 在運(yùn)行中操縱數(shù)據(jù) —— 例如,在輸入代碼時設(shè)置中斷點(diǎn),然后重新設(shè)置傳遞的參數(shù)值以檢查代碼是否能正確處理(現(xiàn)在為)無效的參數(shù)。以這種方式使用調(diào)試程序就不需要讓錯誤條件真正發(fā)生。 設(shè)置斷點(diǎn),然后“單步”通過代碼,這樣您就可以看到每一行代碼所做的事情。 設(shè)置對變量的“監(jiān)視(watch)”。 強(qiáng)制使用錯誤代碼路徑。 觀察調(diào)用堆棧以檢查哪一個例程調(diào)用了您的代碼。 在錯誤發(fā)生時“捕獲”它們。 執(zhí)行邊界檢查。 認(rèn)識您的驗(yàn)證工程師 驗(yàn)證工程師是測試知識的很好來源。他們可以給您指出要測試什么并幫助您了解可以在代碼中加入什么以幫助他們測試(如代碼鉤子)。此外,您可以向他們展示如何使用您的腳手架代碼。他們還會很有興趣了解您認(rèn)為在測試中哪些應(yīng)該是自動化的 —— 如果您某些事情做了不止一遍,那么他們也會。 開始測驗(yàn)! 現(xiàn)在是進(jìn)行小測試的時候了。讓我們看看您是否用心了。 問題 您希望檢查一個整數(shù)的值是否為 5。通常,要這樣編寫代碼: if ( i == 5 ) then ??{ ????// ????// do something... ????// ??} 不過,如果您對代碼進(jìn)行“手指檢查”,并且把代碼寫成了下面這樣會出現(xiàn)什么情況呢? if ( i = 5 ) then ??{ ????// ????// do something... ????// ??} 這個失誤是一個缺陷,但是只有在運(yùn)行時才能捕獲它 —— 可能需要相當(dāng)?shù)恼{(diào)試努力才能找到它。編譯器會輕易放過您的代碼,那么如何防止這種錯誤發(fā)生? 答案 實(shí)際上有兩個答案:您可以使用一種上面描述的靜態(tài)代碼分析工具,并希望它有足夠的健壯性可以捕獲這種錯誤,也可以交換操作數(shù)以使常量位于左邊: if ( 5 == i ) then ??{ ????// ????// do something... ????// ??} 因?yàn)檫@種方法保證您可以在編譯代碼時立即捕捉到問題,所以它是首選的技術(shù)。雖然它看上去有些笨,但是代碼可以編譯并運(yùn)行得很好。然而,當(dāng)您“手指檢查”代碼時就可以立即看到好處了: if ( 5 = i ) then ??{ ????// ????// do something... ????// ??} 可是編譯器不喜歡這樣,因?yàn)?5 不能被賦值為另一個值。這就是我們在 前面 說您應(yīng)當(dāng)將編譯器看成是您的朋友時所表達(dá)的意思。 您還可以在檢查 null 指針時使用這個技巧??聪旅娴拇a: if ( returnString == null ) ??{ ????// ????// do something... ????// ??} 如果您偶然將它“誤寫”成下面這樣會發(fā)生什么呢? if ( returnString = null ) ??{ ????// ????// do something... ????// ??} 您可能不會得到想要的結(jié)果。而改用下面的方法您會得到與我們剛描述過的同樣的“編譯器保護(hù)”: if ( null == returnString ) ??{ ????// ????// do something... ????// ??} 結(jié)束語 為保持簡明扼要我們做了一個相當(dāng)簡潔的歸納:要么現(xiàn)在去做,要么以后花 多得多 的代價去做。換句話說,您在開發(fā)周期的早期在測試和預(yù)防代碼缺陷上花的時間越多,您在以后節(jié)省的時間和金錢就越多。這就是防御性編碼的意義。它就是這么簡單。