「案例要怎麼設計」是寫測試時最常卡住的地方:全部的輸入組合測不完,隨便挑幾個又怕漏掉真正會出包的情況。等價類劃分、邊界值、決策表這幾個技巧,在〈軟體測試基本觀念〉已經整理過,這篇接著講另外兩組常用的設計方法:白箱測試的覆蓋率技巧,還有黑箱測試的狀態轉換測試法。
白箱測試:用覆蓋率設計案例
先看一個容易被覆蓋率數字誤導的例子:
int foo(int a, int b) {
return a / b;
}
測試案例 foo(10, 5),覆蓋率量出來是 100%——這個函數總共就一行程式碼,這一行也確實執行到了。
這真的沒問題嗎?沒有。b = 0 的情況從頭到尾沒被測過,程式一樣會在那個輸入炸掉,但覆蓋率報表看起來完全正常。覆蓋率量的是「程式碼有沒有被執行到」,不是「輸入的所有可能情況有沒有被想到」。100% 覆蓋率是個很好的下限門檻,不是「測試做完了」的保證。
問題在於:不同的覆蓋率定義,門檻鬆緊差很多。由鬆到嚴,常見的有語句、分支、條件、路徑四種。
語句涵蓋率:每行都跑過一次
度量程式碼中每一個可執行的語句是否被執行到了,不含註解、空行這些。
循序語句最單純,把每個語句都執行到即可:
int f(int a, int b)
{
int c;
c = a + b;
return c;
}
覆蓋率 100% 的測試案例:f(1, 2)。
沒有 else 的判斷語句:
int f(int a) {
int b = 0;
if (a > 0) {
b = 1;
}
return b;
}
覆蓋率 100% 的測試案例:f(1)——這一個案例就能讓每一行都執行到,包括 if 裡面的 b = 1。
有 else 的判斷語句:
int f(int a) {
int b = 0;
if (a > 0) {
b = 1;
} else {
b = 2;
}
return b;
}
這次一個案例不夠,因為 b = 1 和 b = 2是兩行不同的程式碼,各自要有測試案例執行到:f(1) 執行 b = 1,f(0) 執行 b = 2。
迴圈語句:
int f(int a) {
for (int i = 0; i <= a; i++) {
Console.WriteLine(i);
}
}
覆蓋率 100% 的測試案例:f(0)。測試案例只要讓迴圈裡的語句執行過一次就達標,沒必要讓它跑很多輪——跑很多輪只會拖長單元測試的時間,涵蓋率並不會因此變高。
多條件的語句(switch):
int f(int a) {
switch (a) {
case 1: f1(); break;
case 2: f2(); break;
case 3: f3(); break;
case 4: f4(); break;
}
}
覆蓋率 100% 的測試案例:f(1)、f(2)、f(3)、f(4),四個 case 各測一次。
分支涵蓋率:每個分支都要走到
語句涵蓋率有個死角:只要求「每行都執行過」,不要求「每個判斷的兩種結果都測過」。回頭看前面沒有 else 的例子:
int f(int a) {
int b = 0;
if (a > 0) {
b = 1;
}
return b;
}
f(1) 就能讓語句涵蓋率衝到 100%,因為程式裡的每一行都執行到了。但 a > 0 為假的那個分支——也就是「跳過 if、直接 return」的路徑——完全沒有測過。分支涵蓋率要求每個判斷式的真假分支都至少執行一次,所以這裡除了 f(1),還要補一個 f(0) 或 f(-1),才能把「假」的分支也測到。
判斷這段程式碼要用幾個測試案例,看的是判斷式的數量而不是行數:有幾個 if、switch,就要準備對應的真假組合。
條件涵蓋率:每個子條件都要測到真跟假
分支涵蓋率只管整個判斷式的真假,判斷式裡如果不只一個子條件,分支涵蓋率是不夠的。
int f(int a, int b)
{
int c = 0;
if ((a > 0) && (b > 0))
{
c = 1;
} else {
c = 2;
}
return c;
}
a > 0 和 b > 0各自都要測到真跟假,四種組合缺一不可:
| a > 0 | b > 0 | 測試數據 |
|---|---|---|
| T | T | a = 1, b = 1 |
| T | F | a = 1, b = 0 |
| F | T | a = 0, b = 1 |
| F | F | a = 0, b = 0 |
有時候就算想全測,覆蓋率還是到不了 100%,因為某些組合在邏輯上根本不可能發生:
int f(int a) {
int c = 0;
if ((a > 0) && (a < 5))
{
c = 1;
}
else
{
c = 2;
}
return c;
}
| a > 0 | a < 5 | 測試數據 |
|---|---|---|
| T | T | 4 |
| T | F | 6 |
| F | T | -1 |
| F | F | 做不到 |
a > 0 和 a < 5 用的是同一個變數 a,F、F 這一列要求 a 同時「小於等於 0」又「大於等於 5」,這種 a 不存在。碰到這種情況,涵蓋率報表停在 75% 是正常的,不代表少測了什麼,是條件本身就排除了這個組合。
路徑涵蓋率:每一條走法都算一次
前面三種覆蓋率看的都是單一判斷式,程式裡有好幾個判斷式接連出現時,光是每個判斷式各自涵蓋還不夠——判斷式跟判斷式組合起來,會走出好幾條完全不同的路徑,路徑涵蓋率要求每一條都測到。
這段流程有 3 個語句(S1、S2、S3)、2 個判斷(M1、M2),走法一共有 4 條:
| 路徑 | 走法 | M1(x > 3 && z < 10) | M2(x == 4 || y > 5) |
|---|---|---|---|
| L1 | a→c→e | False | False |
| L2 | a→b→e | True | False |
| L3 | a→c→d | False | True |
| L4 | a→b→d | True | True |
用兩組數字驗證一下:
x = 4, y = 8, z = 5 時,M1 是 (4 > 3 && 5 < 10) = True,走 b 執行 S3;M2 是 (4 == 4 || 8 > 5) = True,走 d 執行 S2,最後執行 S1 收尾——這是路徑 L4(abd),M1、M2 都是 True。
x = 2, y = 5, z = 11 時,M1 是 (2 > 3 && 11 < 10) = False,走 c;M2 是 (2 == 4 || 5 > 5) = False,走 e,直接執行 S1——這是路徑 L1(ace),M1、M2 都是 False。
| 測試案例 | 輸出 | M1 | M2 | 路徑 |
|---|---|---|---|---|
| x=4, y=8, z=5 | k=31, j=0 | True | True | L4 |
| x=2, y=5, z=11 | k=0, j=0 | False | False | L1 |
換另一組數字,也能湊出剩下兩條路徑,一樣達到 100% 路徑涵蓋率:
| 測試案例 | 輸出 | M1 | M2 | 路徑 |
|---|---|---|---|---|
| x=13, y=2, z=5 | k=25, j=2 | True | False | L2 |
| x=4, y=11, z=6 | k=0, j=2 | False | True | L3 |
路徑涵蓋率是四種裡最嚴的:判斷式一多,路徑數量會用指數速度增加,全測完不太現實,實務上通常只對邏輯特別複雜、特別關鍵的函數才做到這個程度。
黑箱測試:用狀態轉換設計案例
狀態轉換測試是很常見的黑箱測試方法,適合用在這種情況:軟體的輸出和行為不只跟這次輸入有關,還跟它之前的執行狀況、之前發生過的事件有關。狀態圖裡的每個狀態,都是被不同事件驅動著轉換過去的,所以基於狀態圖設計出來的測試,就叫狀態轉換測試。
從狀態圖到狀態轉換樹
拿一個音樂播放器當例子,它的狀態圖長這樣:
四個狀態:開始、執行、快轉、結束。狀態圖直接拿來設計測試案例不太方便,要先轉換成一棵「狀態轉換樹」:
樹的節點對應狀態圖的狀態
樹的樹枝對應狀態圖的事件
樹的根節點是狀態圖的初始狀態
樹的終結點是葉節點
每個節點依照狀態圖繼續往下長出樹枝和節點,直到遇到下面兩種情況,就把該節點當成葉節點:從根節點到這個新節點的路徑上,這個狀態已經出現過;或者,這個節點是狀態圖的結束狀態,不需要再往下轉換
在展開樹之前,要先確定「涵蓋到什麼程度」才算測完,常見的有四個層次,一個比一個嚴格:涵蓋所有狀態、涵蓋所有事件、涵蓋所有狀態轉換至少一次、涵蓋所有狀態、事件與轉換的組合。0-switch 和 1-switch 就是對應不同嚴格程度的具體做法。
0-switch:每個轉換測一次
0-switch 把狀態圖展開成樹,只走到「每個轉換都至少出現一次」的深度:
樹展開之後,從根節點到每一個葉節點的路徑,就是一個測試案例。音樂播放器這個例子展開出 4 條路徑:
開始 → 執行 → 快轉 → 執行
開始 → 執行 → 快轉 → 快轉
開始 → 執行 → 結束 → 執行
開始 → 執行 → 結束
1-switch:連續兩個轉換一起測
0-switch 顧到「每個轉換」,1-switch 再往下一層,顧到「每兩個連續轉換」的組合。同樣的音樂播放器例子,1-switch 展開出 7 條路徑:
深度越深,案例數通常也跟著變多,這點跟白箱測試的路徑涵蓋率是同一個道理:測得越徹底,案例數成長得越快,要在涵蓋程度和測試成本之間抓一個實際可行的深度。
換一個例子:業務流程也是狀態轉換
狀態轉換測試不是只能套在播放器這種介面元件上,一般的業務流程也是一種狀態轉換。以購物流程為例:
用同樣的方法把狀態圖展開成樹,取根節點到每個葉節點的路徑,5 條測試案例涵蓋了登入、修改資料、重設密碼、瀏覽商品、加入購物車到結帳這整條路上的各種走法:
流程越複雜,狀態轉換樹能展開的路徑就越多,但方法完全一樣:先畫出狀態圖,決定要測到哪個嚴格程度,再展開成樹取出測試案例。