軟體測試:哲學視角

殷亮發表於2015-05-07

從直觀上說所有真正的檢驗都是試圖給理論“下絆子”:它不但是一場嚴格的考試,而且是一場不近人情的考試——其目的在於使考生們落敗考場,而不是給考生們以機會去展示他們所知道的東西。展示所知的態度是那些打算去確證或“證實”其理論的人的態度。

——卡爾•波普爾(Karl Popper)[1]

證實與證偽

人們通常把軟體測試視為一種歸納並推論過程,尤其是在功能性測試(functional testing)方面,我們藉助邊界值、等價類、決策表等測試設計方法,通過設計與執行部分用例來推斷整體。比如,對於一個只接受1~255的輸入,如果按照邊界值的方法,我們選擇檢驗1、2、128、254、255,即可推斷程式是否執行正確。也就是說在這裡說我們通過觀察1/51場景的測試結果,來推論50/51未經測試場景的結果。

這樣的測試必然是不完備的,從邏輯的觀點來看,顯然不能證明從單稱陳述(某個場景沒問題)中推論出全稱陳述(所有場景都沒問題)是正確的。就像我們不能從單稱陳述“昨天太陽從東方升起”或“今天太陽從東方升起”來推論全稱陳述“每一天太陽都從東方升起”一樣。在上例中,實際上我們是可以對255個場景進行完備的測試的,但對於絕大多數需要測試的軟體系統,測試工作者面對的往往是無窮的場景。現在讓我們考慮一個很簡單的功能:用滑鼠點選螢幕上的一個按鈕,滑鼠按鍵鬆開後系統彈出OK的提示。在不檢查實現程式碼的情況下,我們並不能保證在按鈕的不同座標位置點選後會出現同樣的結果,也不能保證從按下到鬆開的不同延時會出現同樣的結果,另外,即使是同樣的座標、同樣的延時,要預期第一次點選與第二次點選會出現同樣的結果似乎也是武斷的。儘管這些條件看上去有些不切實、甚至帶有詭辯的味道,但從實現的角度來講,這些都是有可能的:因為程式設計人員完全可以在程式碼中人為地設定一個讓人意想不到的複雜條件,來觸發一個後門、或者一個非常規事件——但它卻是再多的測試都不一定能發現的。因此,不管是操作步驟與操作環境的複雜組合,還是道德風險的可能性,都使得能證明軟體有缺陷的潛在場景已近乎無限大。這也就意味著:即使只是單純地針對軟體中某一個功能做完全驗證,也是幾乎不可能的。

而在早先的測試文獻中我們也不難找到類似的論述,比如在Weinberg 1986中,傑拉德•溫伯格(Gerald Weinberg)就指出我們無法測試所有的可能:“首先,人類的大腦不僅會犯錯誤,而且它的容量也是有限的。其次,沒有人的生命可以無限長。所以無論我們多想進行所有可能的測試,也無法全部考慮到;即使全部考慮到了,也無法活得足夠長來將它們進行完” [2]。而在Patton 2006中,羅恩•巴頓(Ron Patton)也強調了完全測試程式是不可能的,主要原因是“輸入量太大;輸出結果太多;軟體執行路徑太多;軟體說明書是主觀的” [3]。在這些早先的文獻中,都如上文一樣,強調了因為操作組合與環境組合的難以窮盡給質量證明所帶來的困難。然而從哲學的視角看,導致潛在場景難以窮盡的主要因素還並非海量的操作組合與環境組合,而是時空。因為從時空的角度看,即使是對一特定操作之結果的論斷也是一個全稱陳述。也就是說諸如“在裝置A上執行程式,並在按鈕B的座標(15,15)上點選滑鼠,系統顯示結果C”這樣一個陳述實際上就已包含了無窮多個可能的場景,因為它斷定該陳述在一切時空中都為真。但這顯然是無法證實的,因為即使我們昨天觀察到它為真、今天觀察到它為真,甚至過去數百次觀察都是如此,我們也仍不能斷言在下次觀察中必然為真。因此我們可以說,近乎無窮的操作組合與環境組合使得我們事實上無法證明軟體的完美,但真正無窮的時空因素卻是使得這類證明在理論上就已不可能的根本原因。

軟體測試並不能證明軟體是完美的,不管受測的軟體是多麼簡單都是如此,當我們從時空的維度來重新審視軟體時,我們感受到了歸納的無力,在實證領域,不存在所謂的歸納。就像觀察到數以億計的人說漢語,也無法確保所有的人都說漢語一樣——在時空的無限論域中,一種假設哪怕是得到了再多的經驗證據支援,其邏輯概率也永遠趨於零。所以,我們不可能通過測試來宣稱或者斷言軟體的完美。當然,這並不是說世界上不可能存在完美的軟體,而是隻說——我們有可能開發出了完美的軟體而不知。

與證實全稱陳述的複雜性和不可能相比,要證偽一個全稱陳述卻是相對的簡單,因為儘管有再多的觀察也不能推論出全稱陳述為真,但反過來,只要存在一例觀察不為真我們便推倒了整個全稱陳述。這就是哲學家卡樂•波普爾(Karl Popper)所說的證實與證偽之間存在的“令人震驚的不對稱”:“單一的觀察陳述集合有時可以證偽或反駁一個全稱定律,但它不可能在確立定律的意義上證實定律。這個事實的另一個表述為:單一的觀察陳述集合可以證實一個存在陳述(這意味著證偽一個全稱定律),但無法證偽該存在陳述。這是基本的邏輯情境,它表明了一種令人震驚的不對稱” [4]。當我們證偽一個全稱陳述時,實際上是證實了“存在著某一例觀察結果與全稱陳述與推論不符”。在軟體測試中,如果我們拒絕“某軟體是完美”這一論述,那麼必然是我們證實了在該軟體中至少存在著一例缺陷。我們將諸如“軟體存在缺陷”、“上帝是存在的”與“世間有黑天鵝”等稱之為存在陳述,這些陳述在形式上都是存在主義的。與全稱陳述的不可證實但可被證偽相反,存在陳述是可證實但不可被證偽的。在理論上,我們要證實這些陳述是有可能的(至少是潛在可能),但卻無法否定它們。也就是說,即使某人終此一生都未目睹過上帝,又或者我們之前所見到的一萬隻天鵝全都不是黑色的,我們也不能反駁它們的存在;與之相反,哪怕是隻要有一例觀察與陳述相符,這些陳述就得到了證實。因此,在關於軟體缺陷的論述中,我們可以證實某個軟體確實存在缺陷,但卻無法證偽它:即證明其“不存在”缺陷、或者認為它是完美的。事實上,我們甚至不需要進行任何的測試,也能大膽地去評價任何軟體、斷定它們“存在缺陷”,而不用擔心會遭到何種反駁——因為存在陳述是不可證偽的。

這是一個有趣的結論。如果軟體測試工作者的任務是判斷軟體是否有缺陷的話,那麼可以說這份工作既是最容易的,也是最困難的。它的容易之處在於我們總是可以無任何後顧之憂地給出存在缺陷的判斷,而困難則是我們永遠也無法通過努力來證明軟體無缺陷。在現實世界中,人們往往會寄望測試能證明軟體的完美,或者讓測試工作者確保此時此地檢驗了的測試場景,在彼時彼地也能按預期執行,但這卻恰恰是測試最不可能完成的任務。

測試與決定

測試工作者在測試過程中所得到的發現與知識,實際上既不足以證明軟體的完美,也不足以決定後續的行動。測試是一種批判、一種實驗,如果說實驗是一切知識的試金石,那麼測試就是我們對軟體所宣稱的質量的試金石。測試能告知我們的是:軟體經受了何種廣度與強度的檢驗,以及它在什麼場景下存在著缺陷,但也僅此而已。我們通過測試來獲取資訊,而非給出預言。相反,測試恰恰是對來自開發人員對軟體質量的預言(這個軟體沒問題)的檢驗與批判。正如在物理學中的分工那樣(理論物理學家進行想象、推演和猜測新的定律,但並不做實驗;而實驗物理學家則進行實驗、想象、推演和猜測),測試工作就是想盡一切辦法去尋找問題、去讓軟體失敗,除此無他,與證明扯不上半點關係。努力去維護受測系統、去證明我們的軟體是多少的正確,這從來就不是可取的測試精神,測試工作者永遠不會說受測軟體是無缺陷的,而只會說這軟體目前通過了眼下的檢驗,我們還沒有發現否定它的理由。然而這一肯定的判決也只能是暫時的,我們仍對隨後的否定判決開放。因此,測試可以看作是一個探索的過程、一個批判的過程,它獲取資訊,而不做出決定。測試不負責決定軟體是否可以釋出、也不負責決定我們是否要取消某個專案,甚至都無法決定接下來是否還需要進一步的測試。測試過程中所產生的資訊,對於這些決定來說是重要的、也是關鍵的,但我們也必須承認,這些資訊並不充分。

如果說測試永遠是不完備的,只能告知我們當下軟體經過了何種程式的檢驗,那麼,對於軟體釋出後它在客戶處的表現、會不會出現問題、以及會出現什麼問題,理論上我們都只是推斷與猜測。在這一過程中,我們通過已檢驗的場景來推斷未經檢驗的場景,通過這一時空的檢驗結果來推斷另一時空的執行結果。在做出是釋出軟體還是繼續測試的決定時,實際上我們面臨著一種兩難,因為這裡面存在著兩類可能的錯誤:第一類錯誤是軟體還有未找出的缺陷但我們過早地釋出了,而另一類錯誤則是推遲釋出但沒並沒有找出新的缺陷。但是遺憾的是,在這兩類可能的錯誤之間,我們別無選擇,只能選擇盡力避免其一,而不能選擇兩者都避免。因為我們越是試圖控制一類錯誤的發生概率,反過來就越是加大了犯另一類錯誤的可能性。

在統計概念中,這稱之為假設檢驗。在進行假設檢驗時,我們將設定一個假設為原假設,用H0表示,在這裡它表示

H0:軟體還存在可能找出的缺陷

第二個假設稱為備選假設或研究假設,用H1表示。在決定是否應該繼續測試時,它表示

H1:軟體不存在可能找出的缺陷

如此一來,我們設定了兩個互為矛盾的假設,在真實世界裡只能有其一為真。然而,就像前文中所提到的,我們有可能開發出完美的軟體而不知;因此一般來說,我們並不知道、也不能斷定哪個假設是正確的,我們只能猜測。當我們進行假設檢驗時,實際上會存在兩種可能的錯誤。第一類錯誤是當原假設正確時,我們卻拒絕了它。第二類錯誤被定義為當原假設有錯誤時,我們卻沒有拒絕。在上面的例子中,第一類錯誤就是進一步測試還能發現更多的問題(H0為真),但我們卻匆忙釋出了軟體(拒絕了H0);而當我們為了保險進行了一些冗餘測試(拒絕了H1),但並沒有發現問題時(H1為真),第二類錯誤就發生了。我們把發生第一類錯誤的概率記為α,通常它也被稱作顯著水平;第二類錯誤發生的概率記為β。發生錯誤的概率α和β是相反的關係,這就意味著任何嘗試減少某一類錯誤的方法都會使另外一類錯誤發生的概率增加。在決定是釋出還是繼續測試時,如果我們試圖減少第一類錯誤(還能進一步找到缺陷,但沒有繼續測試),我們就必須進行更多更完備地測試,但這一決定無疑就增加了犯第二類錯誤(做了更多的測試,但沒有進一步發現缺陷)的可能性。反過來也是如此,當我們修復一個區域性缺陷而不進行完整測試的時候,實際上我們是希望避免發生第二類錯誤的可能性,但這樣做我們就肯定增加了犯第一類錯誤的可能性。

第一類錯誤(匆忙釋出軟體)的成本是客戶故障成本、商譽蒙塵或法律風險等,而第二類錯誤(推遲軟體的釋出)的成本則可能是冗餘的測試成本、商機延誤或未能儘早地得到市場反饋。有些產品犯第一類錯誤的成本較高,比如航天軟體與醫療軟體,因此人們往往會進行更多更完備的測試,寧願做了許多事後看來無謂的測試、犯第二類錯誤,也要全力避免第一類錯誤發生。另外一些軟體則相反,比如網際網路產品、或是一些市場處於生命週期早期的產品,在這些領域,商機往往是瞬息萬變、稍縱即逝的。在此若是因為犯第二類錯誤而導致商機延誤,與因第一類錯誤引起的軟體故障相比可能會嚴重得多。因此,綜合來看,需要做多完備的測試、何時釋出軟體?這實際上是判斷哪一類錯誤更嚴重的問題,不同的行業會有不同的選擇,不同的競爭者也會有不同的選擇,而測試本身並不足以做出這一選擇。這裡面涉及到的商業價值與成本評估問題,是市場工作者的領域,測試可以提供資訊,但絕不能越俎代庖。從上述角度推論,我們可以認為在軟體領域裡,單從測試角度是不足以決定軟體是否可以釋出的,同時也不存在通用的軟體釋出標準。

測試與系統

軟體系統無疑是複雜的,作為一個複雜系統,我們無法證明其正確性。這一點與自然界規律是相似的,也正因為自然界的複雜性與不可知,對於自然科學的規律,我們只能證偽不能證實。然而,儘管同是因為複雜性導致了軟體行為與自然界行為的不可證實,但這兩類系統的複雜性來源與複雜程度卻是不同的,認識到這點將有助於我們進一步理解軟體測試。經濟學家肯尼思•博爾丁(Kenneth Boulding)根據複雜程度,將系統分為九大型別[5]:

靜態架構 擁有靜態結構,如晶體裡原子的排列或動物的骨骼。

時鐘結構 具有預定運動模式的簡單動態系統,如鐘錶和太陽系。

自調系統 具有根據某些外給目標或準則自我調節的能力,如恆溫器。

開放系統 能夠通過從環境獲取資源進行自我維繫,如生物細胞。

衍生系統 不是通過複製,而是通過含有成長指令的種子或卵進行繁衍,例如雞和蛋系統。

內像系統 具有獲取資訊詳盡地認識環境的能力,以及把這些認識組織成關於環境整體形象或知識結構的能力,這是動物能力所能達到的層次。

抽象系統 具有自我意識,能夠運用語言,人類行為處於這一層次。

社會系統 由具有第7層次能力並遵循共同的社會秩序與文化的行動者組成。

絕對和必然不可知的系統

博爾丁的分類給我們許多啟發。首先,它展示了世界上系統存在的廣泛性與多樣性。其次,我們在上述分類中可以看出軟體系統與自然系統之間的不同:它們分別屬於第3層與最為複雜的第9層。

作為第9層的自然系統,其複雜性在於,我們對於它內部機制與目標完全不可知。無論科學如何發展,科學都不是一個確定的或即成的陳述系統,也不是一個朝著一個終級狀態穩定前進的系統,而僅僅只是關於真實世界的猜想。已故的諾貝爾物理學獎得主理查德•費恩曼(Richard Feynman)有一個關於自然系統的生動比喻:”可以作一想象:組成這個世界的運動物體的複雜排列似乎有點像是天神們所下的一盤偉大的國際象棋,我們則是這盤棋的觀眾。我們不知道弈棋的規則,所有能做的事就是觀看這場棋賽。當然,假如我們觀看了足夠長的時間,總歸能看出幾條規則來……除了我們還不知道所有的規則外,我們真正能用已知規則來解釋的事情也是非常有限,因為幾乎所有的情況都是極其複雜的,我們不能領會這盤棋中應用這些規則的走法,更無法預言下一步將要怎樣。” [6]大自然的複雜性與內在機制的不可知使得我們一切有關這個世界的知識都只是猜測,而證實與證偽之間驚人的不對稱又使得我們即使找到了真理的時候也無法知道這一點。因此對於自然科學,波普爾說:“我們不知道,我們只能猜測”。

與自然系統相比,軟體系統有著與之截然不同的複雜性來源。作為人類科技與意志的產物,我們並非對其內部機制與目的不可知,相反,是我們設計了它的內部機制與目標,可以說,我們是它的造物主。但我們人作為造物主,與自然系統萬能的造物主相比,又是極為渺小和有限的。在構造一個有大量執行路徑的軟體系統的過程中,我們難免會想當然、會忘記、會迷糊、也會疏忽,甚至還有可能存在道德風險,這些都使得我們不應當將軟體視為一個必然會遵從設計與預期的系統,它是複雜的、不確定的,因而也是需要測試與批判的。一方面,是人無與倫比的智慧賦予了它遵從預期行動的能力,另一方面,也是人不可避免的侷限性為其埋下了可能偏出預期的種子。也就是說,其行為的不可預知不在於機制的不可知,而是我們並不能保證我們能完全不出錯地將設計的機制轉換為軟體所預期的行為。正是因為這種結果的不可保證性,我們才需要測試——我們設計了內部機制但無人能保證它的實現。

這就是軟體系統的兩面性,一方面,我們不能因為知其既定的機制,就確信它一定能按照這些機制來實現其行為,因而我們需要懷疑、需要忘掉這些機制去測試它的各種輸入與輸出,黑盒測試在任何時候都是必不可少;另一方面,我們又不能將其看作更高複雜層次的系統,假裝我們對其內部機制一無所知。如此一來,我們實際上是完全放棄了原本可以從內部理解其行為的便利,而這樣的測試設計必然是欠缺效率的,因而從白盒的角度分析軟體系統也是同樣必不可少。

今天,關於軟體的複用,我們已經取得了長足的發展。從函式級複用到元件級複用、再到框架級複用,軟體複用一方面縮短了軟體系統的開發週期,另一方面也減少了缺陷的引入。顯然,如果我們不關心軟體的內部機制的話,那麼測試人員將不能從軟體複用中得到任何好處:因為站在黑盒的角度,我們不能對軟體的實現作任何假設,理論上我們必須測試一切視窗控制元件,即使這些視窗控制元件是複用的,早已經過了千錘百煉;我們也必須測試一切底層API,即使這些底層API都是標準的系統呼叫。但是,如果我們能正視這些機制、利用這些機制的話,我們的測試將更有針對性,也更有效率。我們不應該認為測試應該從零開始,就像我們不應該認為一切科學理論都應該從零開始一樣。在科學的各個領域裡,往往都只存在著少數的一些基本的公理,在這些少數公理的基礎上,科學家們藉助大量的次級理論構建出了巍巍的科學大廈。因此,絕大多數的軟體系統與科學理論一樣,實際上都不是孤立地存在的,它們都包含了一些暫時經受了批判的部分。當我們檢驗系統時可以選擇將這一部分看作是背景知識,暫時地接受,而將精力更多地集中在檢驗那些更脆弱、更容易發現缺陷的部分。

小結

在經典著作《人類理解研究》中,哲學家大衛•休謨曾如此設問並回答:“我以前所食的那個麵包誠然滋養了我,那就是說,具有那些可感性質的那個物體在那時候,是賦有那些神祕的能力的。但是我們果能由此推斷說,別的麵包在別的時候也一樣可以滋養我、而且相似的可感性質總有相似的神祕能力伴隨著它麼?這個結論在各方面看來都不是必然的。”[7]休謨因而指出了歸納的侷限性,即我們不能因為曾經看到那樣一個物象總有那樣一個結果伴隨著它,就可以預測相似的物象也會有相似的結果。在大自然中,這種不可預測性是因為自然系統的不可知。而對於軟體測試,儘管我們知道不少,但我們也不能因觀察到軟體系統的某一功能在此時此地沒問題,因而斷定在客戶處也必然是沒問題的。在要求對事物做出準確預測的時候,只要我們沒有把握到事物的必然性,那麼不管是對事物內部有一知半解還是完全一無所知,實際上是沒有區別的。因此,在探討關於測試我們知道什麼這一問題時,我們大可將軟體視為實現上不可知的系統,這是測試的本質,也是測試工作的起點。然而,當我們探討如何去測試與批判一個系統時,則將軟體系統視為在一定程度上是可知的又是非常有必要的,因為這是關於效率的問題。因此,我們如何看待受測系統,取決於我們需要回答或解決的是哪類問題。但無論如何,軟體測試都是批判而非證明:從不可知的觀點出發,我們不能證明;而從有限可知的觀點看,又有太多的東西可以不去證明。忙亂地寫著冗餘的測試用例的測試人員,就像是在路燈下尋找車鑰匙的人一樣,我們不能說他錯了,但他肯定不可能是卓有成效的。優秀的測試工作者是將測試看作是批判、是藝術的人,他們勇敢地承認自己的侷限性,卸下了證明的重負,站在巨人的肩上,然後看得更遠。

註釋

[1]: Karl Popper. 《Realism And The AIM of Science》. 1983.

[2]: Gerald M. Weinberg. 《Perfect Software: And Other Illusions about Testing》. 1986.

[3]: Ron Patton. 《Software Testing》. 2006.

[4]: Karl Popper. 《Realism And The AIM of Science》. 1983.

[5]: Kenneth E. Boulding. 《General System Theory: The Skeleton of Science》. Management Science, 2. 1956.

[6]: Richard Feynman. 《The Feynman Lectures On Physics, Volume I》. 1963.

[7]: David Hume. 《An Enquiry Concerning Human Understanding》. 1927.

相關文章