【第153期 June 5, 2010】
 

研發新視界

論軟體測試(Software Testing)

作者/葉韶峰

[發表日期:2010/6/5]




前言

軟體完成,除了功能測試之外,效能測試及壓力測試等各種測試流程更是大型軟體系統必備的工作。即使對一項軟體開發工程投注了龐大的心血,如果測試不合格還是枉然,因為客戶要的是「合格產品」,而不是你的「努力過程」。

軟體測試(英文:Soft Beta Test),描述一種用來促進鑑定軟體的正確性、完整性、安全性、和品質的過程。據此,您可能會想,軟體測試永遠不可能完整的確立任意電腦軟體的正確性。換句話說,軟體測試是一種實際輸出與預期輸出間的稽核或者比較過程。

軟體測試的經典定義是:在規定的條件下對程式進行操作,以發現程式錯誤,衡量軟體品質,並對其是否能滿足設計要求進行評估的過程。許多人談到測試,總是有滿腹牢騷,因為它似乎是一件「知易行難」的麻煩工作。為何測試總是做不好?大致可歸類成下述三大原因。

一、測試排最後

目前一般的軟體開發工作,大多採用傳統的「瀑布式(Waterfall)」流程法,也就是把開發過程分為「需求」、「分析」、「設計與撰寫」、「整合」、「測試」等階段,一個接一個依序進行。

這種方法很單純,但導致「測試總是排在最後才進行」的狀況。這種設計會產生兩個狀況:一是測試人員直到案子接近尾聲才上工,所以往往在尚未瞭解整個系統架構的情況下,就一頭栽進工作。二是這個時間點距離完工期實在太近,如果有什麼突發狀況,往往導致整個開發專案大亂或失敗。

二、時間不夠

第二個原因是「時間不夠」,這是開發團隊最常面臨的問題。它其實也是上述「瀑布式」流程法把測試排在最後所導致的另一個致命傷。由於許多開發團隊把大部分時間分配給程式設計與撰寫部分,只留少數時間給測試工作。然而突發狀況永遠無法預期,如果前面階段因故導致工作拖延,在出貨時間不能延後的情況下,後面階段的時間就會不斷地被犧牲。

另外還有一個與「時間不夠」剛好相反的現象,就是「時間太長」。有時產品經初步測試之後發現問題叢生,實在無法交差(或是被客戶退件),所以開發團隊只好回頭繼續進行大量又重複的「測試 → 修改 → 驗證」工作。但通常這將花費大半的時間!

三、風險太高

第三個問題是「風險太高」,也是流程設計不當所致。一個專案的各種隱藏性風險,往往是透過「測試」 才被完整發掘出來。但是「單向瀑布式」流程法卻把測試集中在最後進行,所以它的風險容易隨著開發流程的推進而越來越高;是一種相當危險的風險控管方式。事實上,這也是許多專案在後期才突然出現成本失控或失敗的重要原因。因為等到風險爆發之時,往往已經無力回天,或必須付出相當大的代價以為因應。

解決之道:採用RUP流程

該如何解決這些問題?很簡單,因為問題核心出在流程設計,所以解決之道必須摒棄傳統的「單向瀑布式」流程法,改採「往復式」(Iterations)的 RUP(Rational Unified Process)流程。它將一個準備開發的系統拆解成好幾個子系統,然後不斷往復循環整個開發流程,直到最終成品。

在RUP標準作業規範裡,測試是從一開始就進行的。首先,當客戶的需求被具體化與文件化之後,測試人員就會配合擬定相對的測試計畫,這個工作隨著客戶需求進入分析等後續階段,不斷被追蹤與進行細部修改,逐步接近系統層次。

然後,當任一子系統(模組)在初期階段被開發出來,它的測試工作就會立刻進行,檢驗它是否完全吻合客戶需求與各項預定條件,如此就完成了一項子系統(模組)的所有開發工作。這也就是RUP所揭示的一項重要原則:「產品先出來,測試先行」。在整個開發流程裡,從單元、模組、模組整合到最終產品,測試工作始終依此原則不斷地進行。

在RUP流程裡,測試人員從「一開始」就進入整個開發工作,所以是在完全理解並親身投入產品建置過程的情況下進行工作,不用等到最後階段才嘗試瞭解他所要測試的「到底是什麼東西」,自然可以避免瞎子摸象的危險。

其次,在RUP流程裡,測試人員的工作時間並不是集中在最後階段,而是平均分佈到整個開發流程,隨著過程推展而持續進行測試工作,所以測試人員根本不用擔心時間被佔用而到最後才拼命趕工。

最後,就是深刻影響開發專案成敗的「風險問題」。在RUP流程裡,我們確定「測試先行」的原則,特意讓可能隱藏的高風險在前面階段就先被發覺,然後以逐步降低的方式分散風險,所以不會有傳統瀑布式流程法「風險越來越高」的危機。

測試的進程:Alpha > Beta

Alpha測試:

Alpha測試是由一個用戶在開發環境下進行的測試,也可以是公司內部的用戶在模擬實際操作環境下進行的受控測試,Alpha測試不能由coding人員或測試員完成。Alpha測試發現的錯誤,可以在測試現場立刻反饋給開發人員,由開發人員及時分析和處理。目的是評價軟件產品的功能、可使用性、可靠性、性能和支持。尤其注重產品的界面和特色。Alpha測試可以從軟體 coding結束之後開始,或子系統測試完成後開始,也可以在確認測試過程中產品達到一定的穩定和可靠程度之後再開始。有關的手冊等應該在Alpha測試前準備好。

Beta測試:

Beta測試是軟件的多個用戶在一個或多個用戶的實際使用環境下進行的測試。開發者通常不在測試現場,Beta測試不能由程序員或測試員完成。因而,Beta測試是在開發者無法控制的環境下進行的軟件現場應用。在Beta測試中,由用戶記下遇到的所有問題,包括真實的以及主管認定的,定期向開發者報告,開發者在綜合用戶的報告後,做出修改,最後將產品交付給全體用戶使用。Beta測試著重於產品的支持性,包括文件、客戶培訓和支持產品的生產能力。只有當Alpha測試達到一定的可靠程度後,才能開始Beta測試。由於Beta測試的主要目標是測試可支持性,所以Beta測試應該盡可能由主持產品發行的人員來管理。

SD開發測試角度 (單元測試) (測試驅動開發) (JUnit):

以上所描述是從整個產品包含的範圍比較大來做探討,下面部份則是從開發人員的角度來談測試。

我們在開發的同時,往往需要進行單元測試,主要目的就是要保證與判斷是否有Bug是否結果回傳是正確的。所以當單元測試是如此重要,加上Test-Driven 的開發流程理念發萌,每個工程師都應該為自己撰寫的程式進行單元測試,來設計各種不同的元件。

在 Java 進行測試應該就是採用 JUnit 最多,已經儼然成為一個標準,他的本身核心並不複雜,讓測試可以更為簡單,也可以配合ant進行自動化測試,讓整套系統可以透過撰寫的 script 進行完整測試。

單元測試:

一個單元(Unit)是指一個可獨立進行的工作,獨立 進行指的是這個工作不受前一次或接下來的工作的結果影響,簡單的說,就是不與上下文發生關係。如果是在Java程式中,具體來說一個單元可以是指一個方法(Method),這個方法不依賴於前一次運行的結果,也不牽涉到後一次的運行結果。舉例來說,下面這個程式的gcd()方法可視為一個單元:



而下面的gcd()方法不視為一個單元,要完成此計算,必須呼叫setNum1()、setNum2()與gcd()三個方法:



然而要完全使用一個方法來完成一個單元操作在實行上是有困難的,所以單元也可廣義解釋為數個方法的集合,這數個方法組合為一個單元操作,完成一個工作。

不過設計時仍優先考慮將一個公開的(public)方法要設計為單元,而儘量不用數個公開的方法來完成一件工作,以保持介面簡潔與單元邊界清晰。將工作以一個單元進行設計,這可以使得單元可以重用,並且也使得單元可以進行測試,進而促進類別的可重用性。

單元測試(Unit Test)指的自然就是對每一個工作單元進行測試,瞭解其運行結果是否符合我們的預期,例如當您撰寫完MathTool類別之後,您也許會這麼作個小小的測試程式:



這個動作是開發人員很常作的動作,然而您必須自行看著測試程式的輸出結果來瞭解測試是否成功,另一方面,測試程式本身也是個程式,在更複雜的測試中,您也許會遇到測試程式本身出錯,而導致無法驗證結果的情況。

測試驅動開發(Test-Driven Development, TDD):

大多數的程式設計人員,都習慣先將程式寫好,運行它,然後觀看結果是否正確,懂得在設計好程式後,撰寫專門的測試程式的設計人員已經算是難 能可貴。測試驅動開發(Test-Driven Development, TDD)鼓勵您在撰寫程式之前,就先將測試程式完成,之後再根據測試程式的要求,逐步實現您所要設計的程式。

舉個例子來說,今天您要設計一個計算最大公因數的單元,在測試驅動開發中,您會先撰寫下面的測試程式:



接下來就會有下列的程式自然而然產生出來,此程式可以編譯,但當運行測試程式時,只會一直顯示"Test Fail!"的訊息:



而為了通過測試,再依測試程式的設計完成最大公因數的演算內容就會產生:



現在編譯測試程式,然後運行它,就可以得到"Test OK!"的訊息。先寫測試的好處是,測試程式本身即是設計藍圖,依據這個藍圖撰寫的單元,為了符合測試程式的要求,單元必須設計的可以測試,這迫使設計程式時,考慮到單元的低耦合。

JUnit:

JUnit最初是由Erich Gamma與Kent Beck撰寫,為單元測試(Unit Test)的支持框架,用來撰寫與執行重複性的測試,它包括了以下的特性:

‧對預期結果作斷言
‧提供測試裝備的生成與銷毀
‧易於組織與執行測試
‧圖型與文字介面的測試器

要進行測試,首先要設計測試案例(Test Case),先給程式一個假定的輸出,然後運行程式看看在給定的條件之下,是否符合預期結果。在JUnit下,可以繼承 TestCase來撰寫測試案例,並定義您的測試方法,每一個測試方法是以testXXX()作為命名,一個例子如下所示:



assertEquals()方法用來斷定預期結果與單元方法實際的傳回結果是否相同,如果不同則丟出例外,TestRunner 會捕捉例外,並提取當中的相關訊息報告測試結果,這邊使用的是文字模式的TestRunner。

接下來根據測試案例撰寫實際的程式,首先試著讓測試案例能通過編譯:



編譯完成程式之後,接著運行測試案例,您會得到以下的結果:



在測試驅動中,測試案例所回報的結果會是以測試失敗作為開始,您要一步步消除這些失敗的訊息,接下來我們根據測試案例,完成所設計的程式:



再次運行測試案例,您會得到以下的結果:



這次運行沒有問題,測試已經通過!

上述為一個以最單純的JUnit方法先為開發基礎,所開發出來的一個小功能,基本上若是在開發階段,能以這樣的流程去架構整個程式,將可更減少bug的產生!

參考資料:

http://junit.org/

http://www.openfoundry.org/index.php?option=com_content&Itemid=334&id=893&lang=en&task=view

http://zh.wikipedia.org/zh-tw/JUnit