1. 程式人生 > >軟體測試技術之: 白盒測試和黑盒測試

軟體測試技術之: 白盒測試和黑盒測試


一般地,我們將軟體測試活動分為以下幾類:黑盒測試、白盒測試、靜態測試、動態測試、手動測試、自動測試等等。

黑盒測試

黑盒測試又叫功能測試、資料驅動測試或給予需求規格說明書的功能測試。這種測試注重於測試軟體的功能性需求。

採用這種測試方法,測試工程師把測試物件看作一個黑盒子,不需要考慮程式內部的邏輯結構和特性,只需要依據程式的需求規格說明書,檢查程式的功能是否符合它的功能說明。黑盒測試能更好更真實的從使用者角度來考察被測系統的功能性需求實現情況。在軟體測試的各個階段,如單元測試、整合測試、系統測試及確認測試等階段都發揮著重要作用。尤其在系統測試和確認測試中,其作用是其他測試方法無法取代的。

白盒測試

白盒測試又稱結構測試、邏輯驅動測試或基於程式程式碼內部結構的測試。此時,需要深入考察程式程式碼的內部結構、邏輯設計等等。白盒測試需要測試工程師具備很深的軟體開發工地,精通相應的開發語言,一般的軟體測試工程師難以勝任該工作。

靜態測試

靜態測試,顧名思義,就是靜態的、不執行被測物件程式程式碼而尋找缺陷的過程。通俗地講,靜態測試就是用眼睛看,閱讀程式程式碼,文件資料等,與需求規格說明書中的需求進行比較,找出程式程式碼中設計的不合理,以及文件資料中的錯誤。

在進行程式碼的靜態測試時,可以採用一些程式碼走查的工具,如 QA C++、C++ Test等。

動態測試

動態測試即為實際的執行被測物件的程式程式碼,輸入事先設計好的測試用例,檢查程式程式碼執行的結果與測試用例中設計的預期結果之間是否差異,判定實際結果與預期結果是否一致,從而檢驗程式的正確性、可靠性和有效性,並分析系統執行效率和健壯性等效能狀況。

動態測試由四部分組成:設計測試用例、執行測試用例、分析比較輸出結果、輸出測試報告。

動態測試結合使用白盒測試和黑盒測試。

對於白盒測試,常用的測試方法有:語句覆蓋、判定覆蓋、條件覆蓋、判定/條件覆蓋、多重條件覆蓋等等。黑盒測試較為知名的測試方法有:等價類劃分、邊界值分析、因果圖分析、錯誤猜測等。本章將對這些測試方法進行一些簡單的介紹。

白盒測試關注的是測試用例執行的程度或覆蓋程式邏輯結構(原始碼) 的程度。如完全的白盒測試是將程式中每條路徑都執行到,然而對一個帶有迴圈的程式來說,完全的路徑測試並不切合實際。

                                                                                    


2‑1被測試的小程式

如果完全從路徑測試中跳出來看,那麼有價值的目標似乎就是將程式中的每條語句至少執行一次。遺憾的是,這恰是合理的白盒測試中較弱的準則。圖2‑1描述了這種思想。假設圖2‑1 代表了一個將要進行測試的小程式,其等價的程式碼段如下:

Public void foo(int a, int b, int x)

{

if (a > 1 &&b == 0)

{

        x = x/ a;

    }

if (a == 2 || x > 1)

{

       X = x +1;

    }

}

通過編寫單個的測試用例遍歷程式路徑 ace,可以執行到每一條語句。也就是說,通過在點 a 處設定 A=2,B=0,X=3,每條語句將被執行一次(實際上,X 可被賦任何值)。

遺憾的是,這個準則相當不足。舉例來說,也許第一個判斷應是“或”,而不是 “與” 。 如果這樣, 這個錯誤就會發現不到。另外, 可能第二個判斷應該寫成 “X>0” ,這個錯誤也不會被發現。還有,程式中存在一條 X 未發生改變的路徑(路徑 abd),如果這是個錯誤,它也不會被發現。換句話說,語句覆蓋這條準則有很大的不足,以至於它通常沒有什麼用處。

判定覆蓋或分支覆蓋是較強一些的邏輯覆蓋準則。該準則要求必須編寫足夠的測試用例,使得每一個判斷都至少有一個為“真”和為“假”的輸出結果。換句話說,也就是每條分支路徑都必須至少遍歷一次。分支或判定語句的例子包括switch,do-while 和 if-else 語句。

判定覆蓋通常可以滿足語句覆蓋。由於每條語句都是在要麼從分支語句開始,要麼從程式入口點開始的某條子路徑上,如果每條分支路徑都被執行到了,那麼每條語句也應該被執行到了。但是,仍然有些例外情況:

•  程式中不存在判斷。

•  程式或子程式/方法有著多重入口點。只有從程式的特定入口點進入時,某條特定的語句才能執行到。

我們的探討僅針對有兩個選擇的判斷或分支,當程式中包含有多重選擇的判斷時,判定/分支覆蓋準則的定義就必須有所改變。典型的例子有包含select(case)語句的 Java 程式,包含算術 (三重選擇) IF 語句、計算或算術 GOTO 語句的 FORTRAN 程式,以及包含可選 GOTO 語句或 GO-TO-DEFENDING-ON 語句的 COBOL 程式。對於這些程式,判定/分支覆蓋準則將所有判斷的每個可能結果都至少執行一次,以及將程式或子程式的每個入口點都至少執行一次。

在圖2‑1 中,兩個涵蓋了路徑 ace 和 abd,或涵蓋了路徑 acd 和 abe 的測試用例就可以滿足判定覆蓋的要求。如果我們選擇了後一種情況,兩個測試用例的輸入是 A=3,B=0,X=3 和 A=2,B=1,X=1。

判定覆蓋是一種比語句覆蓋更強的準則,但仍然相當不足。舉例來說,我們僅有 50%的可能性遍歷到那條 X 未發生變化的路徑(也即, 僅當我們選擇前一種情況) 。

如果第二個判斷存在錯誤(例如把 X>1 寫成了 X<1,那麼前面例子中的兩個測試用例都無法找出這個錯誤。

比判定覆蓋更強一些的準則是條件覆蓋。在條件覆蓋情況下,要編寫足夠的測試用例以確保將一個判斷中的每個條件的所有可能的結果至少執行一次。因為,就如同判定覆蓋的情況一樣,這並不總是能讓每條語句都執行到,因此作為對這條準則的補充就是對程式或子程式。舉例來說,分支語句

DO K=0 to 50 WHILE (J+K<QUEST)

包含兩種情況:K 是否小於或等幹 50?以及 J+K 是否小於 QUEST? 因此,需要針對K  <=  50、 K  >50 (達到迴圈的最後一次迭代)以及J+K<QUEST、 J+K>=QUEST的情況設計測試用例。

圖 2‑1 有四個條件:A>1、B=0、A=2 以及 X>1。因此需要足夠的測試用例,使得在點 a 處出現 A=2、A<2、X>1 及 X<=1 的情況。有足夠數量的測試用例滿足此準則,用例及其遍歷的路徑如下所示:

1.A=2,B=0,X=4  ace

2.A=1,B=1,X=1  adb

請注意,儘管在本例中生成的測試用例數量是一樣的,但條件覆蓋通常還是要比判定覆蓋更強一些。因為,條件覆蓋可能(但並不總是這樣)會使判斷中的各個條件都取到兩個結果(“真”和“假” ) ,而判定覆蓋卻做不到這一點。舉例來說,在相同的分支語句

DO  K=0 to 50 WHILE(J+K<QUEST)

中,存在一個兩重分支(執行迴圈體,或者跳過迴圈體) 。如果使用的是判定覆蓋測試,將迴圈從 K= 0 執行到 K = 51 即可滿足該準則,但從未考慮到 WHILE子句為假的情況。如果使用的是條件覆蓋準則,就需要設計一個測試用例為J+K<QUEST 產生一個為假的結果。

雖然條件覆蓋準則乍看上去似乎滿足判定覆蓋準則,但並不總是如此。如果正在測試判斷條件 IF (A&B),條件覆蓋準則將要求編寫兩個測試用例:A為真,B為假;A 為假,B 為真。但是這並不能使 IF 語句中的 THEN 被執行到。對圖2‑1 所示例子所進行的條件覆蓋測試涵蓋了全部判斷結果,但這僅僅是偶然情況。舉例來說,兩個可選的測試用例:

1. A=2,B=0,X=3

2. A=1,B=1,X=1

涵蓋了全部的條件結果,卻僅涵蓋了四個判斷結果中的兩個(這兩個測試用例都涵蓋到了路徑 abe,因而不會執行第一個判斷結果為真的路徑,以及第二個判斷結果為假的路徑) 。

顯然,解決上面左右為難局面的辦法就是所謂的判定/條件覆蓋準則。這種準則要求設計出充足的測試用例。將一個判斷中的每個條件的所有可能的結果至少執行一次,將每個判斷的每個條件的所有可能的結果至少執行一次,將每個判斷的所有可能的結果至少執行一次,將每個入口點都至少呼叫一次。

判定/條件覆蓋準則的一個缺點是儘管看上去所有條件的所有結果似乎都執行到了,但由於有些特定的條件會遮蔽掉其他的條件,常常並不能全部都執行到。請參見圖2‑2來觀察此種情況。圖2‑2 中的流程圖描述的是編譯器將圖2‑1中的程式編譯生成機器程式碼的過程。源程式中的多重條件判斷被分解成單個的判斷和分支,因為大多數的機器都沒有能執行多重條件判斷的單獨指令。那麼,更為完全的測試覆蓋似乎是將每個基本判斷的全部可能的結果都執行到,而前兩個判定覆蓋的測試用例都做不到這點,它們未能執行到判斷 I 中的結果為 “假”的分支, 以及判斷 K 中結果為“真”的分支。

                                                                  

2‑2 圖 2‑1中程式的機器碼

如圖2‑2所示,其中的原因是“與”和“或”表示式中某些條件的結果可能會遮蔽掉或阻礙其他條件,的判斷。舉例來說,如果“與”表示式中有個條件為“假”,那麼就無須計算該表示式中的後續條件。 同樣, 如果 “或”表示式中有個條件為 “真” ,那麼後續條件也無須計算。因此,條件覆蓋或判定/條件覆蓋誰都不一定會發現邏輯表示式中的錯誤。

所謂的多重條件覆蓋準則能夠部分解決這個問題。該準則要求編寫足夠多的測試用例,將每個判定中的所有可能的條件結果的組合,以及所有的入口點都至少執行一次。舉例來說,考慮下面的虛擬碼程式;

NOTFOUND=TRUE;

DO I=1 TO TABSIZE WHILE (NOTFOUND); /*SEARCHTABLE*/

 ……searching logic……;

END

要測試四種情況:

1.  I<=TABSIZE,並且 NOTFOUND 為真;

2.  I<=TABSIZE,並且 NOTFOUND 為假(在到達表格尾部前查詢指定條目) ;

3.  I>TABSIZE,並且 NOTFOUND 為真(查詢了整個表格,未找到指定條目) ;

4.  I>TABSIZE,並且 NOTFOUND 為假(指定條目位於表格的最後位置) 。

很容易發現,滿足多重條件覆蓋準則的測試用例集,同樣滿足判定覆蓋準則、條件覆蓋準則以及判定/條件覆蓋準則。

再次回到圖2‑1中,測試用例必須覆蓋以下 8 種組合:

1.      A > 1,  B  =  0

2.      A > 1,  B !=  0

3.      A <= 1,B  =  0

4.      A <= 1,   B != 0

5.      A = 2,  X >  1

6.      A = 2,  X <= 1

7.      A != 2, X  >  1

8.      A != 2, X <= 1

注意,第 5 至8 組合表示了第二個 if 語句的值。由於 X可能在該 if 語句之前發生了改變,因此這個 if 語所需的值必需對程式邏輯進行回溯,以找到相對應的輸入值,要測試的這 8 種組合並不一定意味著需要設計出 8 個測試用例。實際上,用 4 個測試用例就可以覆蓋它們。下面是這些測試用例的輸入,以及它們覆蓋的組合:

A=3,B=0,X=4 覆蓋組合 1,5

A=2,B=1,X=1 覆蓋組合 2,6

A=1,B=0,X=2 覆蓋組合 3,7

A=1,B=1,X=1 覆蓋組合 4,8

圖 2‑1的程式存在 4 條不同的路徑,需要 4 個測試用例,這樣的情況純屬巧合。事實上,這 4 個用例也沒有覆蓋到每條路徑,路徑 acd 就被遺漏掉了。

舉例來說,對於如下所示的判斷語句,儘管它只包舍兩條路徑,仍可能需要 8個測試用例:

if (x==y && length(z)==0 && FLAG)

{

j=1 ;

}

Else

{

    i=1;

}

在存在迴圈的情況下,多重條件覆蓋準則所需要的測試用例的數量通常會遠遠小於其路徑的數量。

總的來說,對於包含每個判斷只存在一種條件的程式,最簡單的測試準則就是設計出足夠數量的測試用例,實現:(1)將每個判斷的所有結果都至少執行一次;(2)將所有的程式入口都至少呼叫一次,以確保全部的語句都至少執行一次。而對於包含多重條件判斷的程式,最簡單的測試準則是設計出足夠數量的測試用例,將每個判斷的所有可能的條件結果的組合,以及所有的入口點都至少執行一次(加入“可能”二字,是因為有些組合情況難以生成)。

在以上測試用例中,我們發現漏掉了路徑acd。路徑覆蓋則要求覆蓋程式所有可能的路徑,路徑覆蓋需要對所有可能的路徑進行測試(包括迴圈、條件組合、分支選擇等)。那麼需要設計大量、複雜的測試用例,使得工作量呈指數級增長。而在有些情況下,一些執行路徑是不可能被執行的。

從這個簡單的例子可以看出,要想充分測試一個程式是很困難的。同時,測試條件越強,測試的代價越高。測試時應分主次,在測試代價和測試充分性之間做出平衡。

一個好的測試用例描述為具有相當高的可能性發現某個錯誤來,此外對程式的窮舉輸入測試是無法實現的。因此,當測試某個程式時,我們就被限制在從所有可能的輸入中努力找出某個小的子集。理所當然,我們要找的子集必須是正確的,並且是可能發現最多錯誤的子集。確定這個子集的一種方法,就是要意識到一個精心挑選的測試用例還應具備另外兩個特性:

1.  嚴格控制測試用例的增加,減少為達到“合理測試”的某些既定目標而必須設計的其他測試用例的數量。

2.  它覆蓋了大部分其他可能的測試用例。也就是說,它會告訴我們,使用或不使用這個特定的輸入集合,哪些錯誤會被發現,哪些會被遺漏掉。

雖然這兩個特性看起來很相似,但描述的卻是截然不同的兩種思想。第一個特性意味著,每個測試用例都必須體現儘可能多的不同的輸入情況,以使最大限度地減少測試所需的全部用例的數量。而第二個特性意味著應該儘量將程式輸入範圍進行劃分,將其劃分為有限數量的等價類,這樣就可以合理地假設(但是,顯然不能絕對肯定)測試每個等價類的代表性資料等同於測試該類的其他任何資料。也就是說,如果等價類的某個測試用例發現了某個錯誤,該等價類的其他用例也應該能發現同樣的錯誤。相反,如果測試用例沒能發現錯誤,那麼我們可以預計,該等價類中的其他測試用例不會出現在其他等價類中,因為等價類是相互交迭的。

這兩種思想形成了稱為等價劃分的黑盒測試方法。第二種思想可以用來設計一個“令人感興趣的”輸入條件集合以供測試,而第一個思想可以隨後用來設計涵蓋這些狀態的一個最小測試用例集。

使用等價劃分方法設計測試用例主要有兩個步驟:(1)確定等價類; (2)生成測試用例。

2‑1等價類列舉表

輸入條件

有效等價類

無效等價類

1.確定等價類

確定等價類是選取每一個輸入條件(通常是規格說明中的一個句子或短語)並將其劃分為兩個或更多的組。可以使用表2‑1中的表格來進行劃分。注意,我們確定了兩類等價類:有效等價類代表對程式的有效輸入,而無效等價類代表的則是其他任何可能的輸入條件(即不正確的輸入值)。這樣,我們遵循了測試原則,即要注意無效和未預料到的輸入情況。

在給定了輸入或外部條件之後,確定等價類大體上是一個啟發式的過程。下面給出了一些指導原則:

1.  如果輸入條件規定了一個取值範圍(例如,“數量可以是從1到999”),那麼就應確定出一個有效等價類 (1<數量<999 ) ,以及兩個無效等價類 (數量<1,數量>999) 。

輸入條件

有效等價類

無效等價類

取值範圍

1<數量<999

數量<1,數量>999

2.  如果輸入條件規定了取值的個數(例如,“汽車可登記一至六名車主”),那麼就應確定出一個有效等價類和兩個無效等價類(沒有車主,或車主多於六個)。

輸入條件

有效等價類

無效等價類

取值個數

1至六名車主

沒有車主,車主多於六個

3.  如果輸入條件規定了一個輸入值的集合,而且有理由認為程式會對每個值進行不同處理(例如,“交通工具的型別必須是公共汽車、卡車、計程車、火車或摩托車” ) ,那麼就應為每個輸入值確定一個有效等價類和一個無效等價類(例如,“拖車” ) 。

輸入條件

有效等價類

無效等價類

輸入值集合

公共汽車,卡車,計程車,火車,摩托車

拖車

4.  如果存在輸入條件規定了“必須是”的情況,例如“識別符號的第一個字元必須是字母”,那麼就應確定一個有效等價類(首字元是字母)和一個無效等價類(首字元不是字母) 。

輸入條件

有效等價類

無效等價類

第一個字元

首字元是字母

首字元不是字母

如果有任何理由可以認為程式並未等同地處理等價類中的元素,那麼應該將這個等價類再劃分為小一些的等價類。

2.生成測試用例

第二步是使用等價類來生成測試用例,我們以上述第一個例子來說明,其過程如下:

1.    為每個等價類設定一個不同的編號。

2.  編寫新的測試用例,儘可能多地覆蓋那些尚未被涵蓋的有效等價類,直到所有的有效等價類都被測試用例所覆蓋(包含進去)。

3.  編寫新的用例,覆蓋一個且僅一個尚未被覆蓋的無效等價類,直到所有的無效等價類都被測試用例所覆蓋。

用單個測試用例覆蓋無效等價類,是因為某些特定的輸入錯誤檢查可能會遮蔽或取代其他輸入錯誤檢查。舉例來說,如果規格說明規定了“請輸入書籍型別(硬皮、軟皮或活頁)及數量(l~999 )”,代表兩個錯誤輸入(書籍型別錯誤,數量錯誤)的測試用例“XYZ 0”,很可能不會執行對數量的檢查,因為程式也許會提示“XYZ 是未知的書籍型別”,就不檢查輸入的其餘部分了。

這裡將上述的第4個例子進行一下擴充套件,然後進行示範說明如何用等價類劃分的思想來設計測試用例:

規定識別符號的第一個字元必須是字母,識別符號只能使用字母、數字和下劃線。

第一步:劃分等價類,為每一個等價類編號。

輸入條件

有效等價類

無效等價類

第一個字元

首字元是字母(1)

首字元不是字母(2)

字元限制

僅使用字母、數字和下劃線(3)

使用了其他字元(4)

第二步:設計測試用例,覆蓋所有有效等價類,用盡可能少的用例覆蓋最多的有效等價類。

測試用例:Test_1                覆蓋(1)(3)

 第三步:設計測試用例,覆蓋所有無效等價類。每新增一個測試用例,只覆蓋一個無效等價類。

測試用例:123test               覆蓋(2)

測試用例:Test#¥              覆蓋(4)

經驗證明,考慮了邊界條件的測試用例與其他沒有考慮邊界條件的測試用例相比,具有更高的測試回報率。所謂邊界條件,是指輸入和輸出等價類中那些恰好處於邊界、或超過邊界、或在邊界以下的狀態。邊界值分析方法與等價劃分方法存在兩方面的不同:

1.  與從等價類中挑選出任意一個元素作為代表不同,邊界值分析需要選擇一個或多個元素,以便等價類的每個邊界都經過一次測試。

比如要求輸入職工年齡,規定輸入為18 – 45。根據等價類劃分思想,一個有效等價類:18<= 年齡 <=45 ,兩個無效等價類:年齡<18 和 年齡>45。這樣選取 10 ,30 ,50即可滿足覆蓋。但是等價類的思想沒有從邊界值方面來分析問題,從邊界值角度分析,我們就會再新增17、18、19 、44、45、46這樣的邊界值。

2.  與僅僅關注輸入條件(輸入空間)不同,還需要考慮從結果空間(輸出等價類)設計測試用例。

 比如ATM機取款手續費為取款額%1,最低2元,最高50元。從輸出等價類來考慮邊界值,就要設計用例來測試手續費會不會低於2元,會不會高於50元。

很難提供一份如何進行邊界值分析的“詳細說明’,因為這種方法需要一定程度的創造性,以及對問題採取一定程度的特殊處理辦法(因此,就像測試的許多其他方面一樣,這更多的是項智力工作,並非其他的什麼)。

邊界值分析和等價劃分的一個弱點是未對輸入條件的組合進行分析。邊界值測試不一定能檢查出此類錯誤。

對輸入組合進行測試並不是簡單的事情,因為即使對輸入條件進行了等價劃分,這些組合的數量也是個天文數字。如果在選擇輸入條件的子集時沒有采用一個系統的方法,很可能選擇出一個任意的輸入條件子集, 這樣會使測試沒有什麼成效。 

因果圖有助於用一個系統的方法選擇出高效的測試用例集。它還有一個額外的好處,就是可以指出規格說明的不完整性和不明確之處。

這裡不對因果圖這種方法進行詳細介紹,有興趣的同事可以查閱《軟體測試的藝術》相關章節。

常常可以看到這種情況,有些人似乎天生就是測試的能手。這些人沒有用到任何特殊的方法(比如對因果圖進行邊界值分析),卻似乎有著發現錯誤的訣竅。

對此的一個解釋是這些人更多是在下意識中,實踐著一種稱為錯誤猜測的測試用例設計技術。接到具體的程式之後,他們利用直覺和經驗猜測出錯的可能型別,然後編寫測試用例來暴露這些錯誤。

由於錯誤猜測主要是一項依賴於直覺的非正規的過程,因此很難描述出這種方法的規程。其基本思想是列舉出可能犯的錯誤或錯誤易發情況的清單,然後依據清單來編寫測試用例。例如,程式的輸入中出現 0 這個值就是一種錯誤易發情況。因此,可以編寫測試用例,檢查特定的輸入值中有 0,或特定的輸出值被強制為 0 的情況。同樣, 在出現輸入或輸出的數量不定的地方 (如某個被搜尋列表的條目數量)。數量為“沒有”和“一個” (例如空列表,僅包含一個條目的列表)也是錯誤易發情況。另一個思想是,在閱讀規格說明時聯絡程式設計師可能做的假設來確定測試用例(即規格說明中的一些內容會被忽略,要麼是由於偶然因素,要麼是程式設計師認為其顯而易見)。