首頁/測試

軟體測試的設計方法

2025年07月18日 測試 白箱測試 黑箱測試 覆蓋率 狀態轉換測試 單元測試

「案例要怎麼設計」是寫測試時最常卡住的地方:全部的輸入組合測不完,隨便挑幾個又怕漏掉真正會出包的情況。等價類劃分、邊界值、決策表這幾個技巧,在〈軟體測試基本觀念〉已經整理過,這篇接著講另外兩組常用的設計方法:白箱測試的覆蓋率技巧,還有黑箱測試的狀態轉換測試法。


白箱測試:用覆蓋率設計案例

先看一個容易被覆蓋率數字誤導的例子:

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 = 1b = 2是兩行不同的程式碼,各自要有測試案例執行到:f(1) 執行 b = 1f(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),才能把「假」的分支也測到。

判斷這段程式碼要用幾個測試案例,看的是判斷式的數量而不是行數:有幾個 ifswitch,就要準備對應的真假組合。

條件涵蓋率:每個子條件都要測到真跟假

分支涵蓋率只管整個判斷式的真假,判斷式裡如果不只一個子條件,分支涵蓋率是不夠的。

int f(int a, int b)
{
    int c = 0;
    if ((a > 0) && (b > 0))
    {
        c = 1;
    } else {
        c = 2;
    }
    return c;
}

a > 0b > 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 > 0a < 5 用的是同一個變數 a,F、F 這一列要求 a 同時「小於等於 0」又「大於等於 5」,這種 a 不存在。碰到這種情況,涵蓋率報表停在 75% 是正常的,不代表少測了什麼,是條件本身就排除了這個組合。

路徑涵蓋率:每一條走法都算一次

前面三種覆蓋率看的都是單一判斷式,程式裡有好幾個判斷式接連出現時,光是每個判斷式各自涵蓋還不夠——判斷式跟判斷式組合起來,會走出好幾條完全不同的路徑,路徑涵蓋率要求每一條都測到。

圖一:複雜的條件——兩個判斷式接連出現,走出 4 條路徑

這段流程有 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

路徑涵蓋率是四種裡最嚴的:判斷式一多,路徑數量會用指數速度增加,全測完不太現實,實務上通常只對邏輯特別複雜、特別關鍵的函數才做到這個程度。


黑箱測試:用狀態轉換設計案例

狀態轉換測試是很常見的黑箱測試方法,適合用在這種情況:軟體的輸出和行為不只跟這次輸入有關,還跟它之前的執行狀況、之前發生過的事件有關。狀態圖裡的每個狀態,都是被不同事件驅動著轉換過去的,所以基於狀態圖設計出來的測試,就叫狀態轉換測試。

從狀態圖到狀態轉換樹

拿一個音樂播放器當例子,它的狀態圖長這樣:

圖二:音樂播放器狀態圖

四個狀態:開始、執行、快轉、結束。狀態圖直接拿來設計測試案例不太方便,要先轉換成一棵「狀態轉換樹」:

  1. 樹的節點對應狀態圖的狀態

  2. 樹的樹枝對應狀態圖的事件

  3. 樹的根節點是狀態圖的初始狀態

  4. 樹的終結點葉節點

  5. 每個節點依照狀態圖繼續往下長出樹枝和節點,直到遇到下面兩種情況,就把該節點當成葉節點:從根節點到這個新節點的路徑上,這個狀態已經出現過;或者,這個節點是狀態圖的結束狀態,不需要再往下轉換

在展開樹之前,要先確定「涵蓋到什麼程度」才算測完,常見的有四個層次,一個比一個嚴格:涵蓋所有狀態、涵蓋所有事件、涵蓋所有狀態轉換至少一次、涵蓋所有狀態、事件與轉換的組合。0-switch 和 1-switch 就是對應不同嚴格程度的具體做法。

0-switch:每個轉換測一次

0-switch 把狀態圖展開成樹,只走到「每個轉換都至少出現一次」的深度:

圖三:0-switch——狀態圖展開成樹,root 到每個葉節點就是一個測試案例

樹展開之後,從根節點到每一個葉節點的路徑,就是一個測試案例。音樂播放器這個例子展開出 4 條路徑:

  1. 開始 → 執行 → 快轉 → 執行

  2. 開始 → 執行 → 快轉 → 快轉

  3. 開始 → 執行 → 結束 → 執行

  4. 開始 → 執行 → 結束

1-switch:連續兩個轉換一起測

0-switch 顧到「每個轉換」,1-switch 再往下一層,顧到「每兩個連續轉換」的組合。同樣的音樂播放器例子,1-switch 展開出 7 條路徑:

圖四:1-switch 測試案例——連續兩個轉換的組合

深度越深,案例數通常也跟著變多,這點跟白箱測試的路徑涵蓋率是同一個道理:測得越徹底,案例數成長得越快,要在涵蓋程度和測試成本之間抓一個實際可行的深度。

換一個例子:業務流程也是狀態轉換

狀態轉換測試不是只能套在播放器這種介面元件上,一般的業務流程也是一種狀態轉換。以購物流程為例:

圖五:購物流程狀態圖

用同樣的方法把狀態圖展開成樹,取根節點到每個葉節點的路徑,5 條測試案例涵蓋了登入、修改資料、重設密碼、瀏覽商品、加入購物車到結帳這整條路上的各種走法:

圖六:購物流程測試案例

流程越複雜,狀態轉換樹能展開的路徑就越多,但方法完全一樣:先畫出狀態圖,決定要測到哪個嚴格程度,再展開成樹取出測試案例。