首頁/測試

白箱測試

2025年07月18日 測試 白箱測試 覆蓋率 分支覆蓋率 條件覆蓋率 路徑覆蓋率 MC/DC

[TOC]

白箱測試

白箱測試看得到程式碼內部結構,設計案例的依據不是輸入輸出,是「程式碼有沒有被跑到」。跑到的程度分成好幾個層級,由鬆到嚴依序是語句、分支、條件、路徑,中間還有判斷/條件涵蓋率和 MC/DC 這兩個進階變體。層級越嚴,案例數通常也跟著變多,這篇按照嚴格程度一層一層往上疊。

圖一:多個判斷式接連出現的控制流程,後面路徑涵蓋測試會詳細拆解

覆蓋率的迷思

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

int foo(int a, int b) {
    return a / b;
}

測試案例 foo(10, 5),覆蓋率量出來是 100%——這個函數總共就一行程式碼,這一行也確實執行到了。

這真的沒問題嗎?沒有。b = 0 的情況從頭到尾沒被測過,程式一樣會在那個輸入炸掉,但覆蓋率報表看起來完全正常:

圖二:100% 覆蓋率不代表沒問題

覆蓋率量的是「程式碼有沒有被執行到」,不是「輸入的所有可能情況有沒有被想到」。100% 覆蓋率是個很好的下限門檻,不是「測試做完了」的保證。

語句覆蓋測試

度量程式碼中每一個可執行的語句是否被執行到了,因此不含註解、空行等。不同的語句形狀,要達到 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)

有 else 的判斷語句

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

覆蓋率 100% 的測試案例:f(1) 執行 b = 1f(0) 執行 b = 2,兩個案例都要有。

循環語句

int f(int a) {
    for (int i = 0; i <= a; i++) {
        Console.WriteLine(i);
    }
}

覆蓋率 100% 的測試案例:f(0)

測試案例必須讓循環體內的語句必須有執行一次,如果讓它執行很多次,有可能單元測試的時間會變長,而且意義不大。

多條件的語句

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)


分支覆蓋測試

語句涵蓋率有個死角:只要求「每行都執行過」,不要求「每個判斷式的真假兩種結果都測過」。分支涵蓋率補上這一塊:每個判斷式的真分支、假分支都至少要跑過一次。拿前面同一批例子繼續看,哪些例子分支涵蓋率會多要求案例,哪些不會:

圖四:分支涵蓋率比語句涵蓋率多要求什麼

沒有 else 的判斷語句

f(1) 就能讓語句涵蓋率衝到 100%,因為每一行都執行到了。但 a > 0 為假的那個分支——直接跳過 if、return b = 0——完全沒有測過。分支涵蓋率要求真假分支都至少一次,所以除了 f(1),還要補 f(0)f(-1),兩個案例才夠。

有 else 的判斷語句

這個例子語句涵蓋率原本就需要 f(1)f(0) 兩個案例(因為 b = 1b = 2 是不同行)。這兩個案例剛好也把 a > 0 的真假分支都測到了,分支涵蓋率不需要再多加案例——語句涵蓋率和分支涵蓋率在這裡要求一樣多。

循環語句

迴圈的判斷式是 i <= af(0) 執行一次迴圈:第一次檢查 0 <= 0 為真,跑完 i 變 1,再檢查 1 <= 0 為假,跳出迴圈——這一個案例剛好把真假分支都測到了,分支涵蓋率不用多加案例。

不過「迴圈一次都不跑」(例如 f(-1))雖然不是分支涵蓋率硬性要求的,實務上還是建議測。這種邊界常常是 bug 藏的地方,只是這已經不算分支涵蓋率的範圍,是邊界值測試在管的事了。

多條件的語句

f(1)f(2)f(3)f(4) 涵蓋了四個 case 各自的分支,但這個 switch 沒有 defaulta 不是 1~4 的情況(例如 f(5))走的是「什麼都不做」這條隱性分支,四個案例完全沒測到。分支涵蓋率要求這條分支也要測,得再補一個 f(5)(或其他不在 1~4 的值)。這種「沒有 default 的 switch」是很容易漏測的地方,程式收到意外輸入時悄悄什麼都不做,比丟例外還難發現。

複雜的條件

多個判斷式接連出現時,分支涵蓋率只管「每個判斷式各自的真假」,不管「判斷式之間的組合走了哪幾條路」——這一點跟後面路徑涵蓋率的要求差很多,用同一個例子對照最清楚:

圖五:複雜的條件——兩個判斷式接連出現

這段流程有 3 個語句(S1、S2、S3)、2 個判斷(M1:x > 3 && z < 10、M2:x == 4 || y > 5)。分支涵蓋率只要求 M1 測過真跟假、M2 也測過真跟假,不要求特定組合,兩個測試案例就能達標:

測試案例 M1 M2
x=4, y=8, z=5 True True
x=2, y=5, z=11 False False

這兩個案例把 M1、M2 的真假都測過一次,分支涵蓋率 100%。但對照後面〈路徑涵蓋測試〉會看到,這條流程實際上有 4 條走法,這兩個案例只測到其中 2 條(M1、M2 都真的那條,跟都假的那條),完全沒測到「M1 真 M2 假」和「M1 假 M2 真」這兩條路徑。分支涵蓋率 100% 不等於路徑涵蓋率 100%,這就是它比路徑涵蓋率鬆的地方。


條件覆蓋測試

分支涵蓋率只管整個判斷式的真假,判斷式如果是好幾個子條件用 &&|| 接起來,分支涵蓋率是不夠的——子條件各自的真假也要測到。

前面「沒有 else」「有 else」「循環」「多條件」這幾個例子的判斷式都只有一個子條件(a > 0i <= a),沒有可以拆的複合條件,條件涵蓋率在這幾個例子上跟分支涵蓋率要求一樣,沒有新東西可以測。真正用得上條件涵蓋率的,是判斷式裡有 &&|| 的情況。

複雜的條件

還是用 M1(x > 3 && z < 10)這個判斷式來看。M1 有兩個子條件:x > 3z < 10,條件涵蓋率要求這兩個子條件各自的真假都要測到:

圖六:條件涵蓋率——兩個子條件湊出 4 種組合

x > 3 z < 10 測試數據
T T x=4, z=5
T F x=4, z=15
F T x=2, z=5
F F x=2, z=15

M2(x == 4 \|\| y > 5)用一樣的方法各自測 x == 4y > 5 的真假組合,這裡不重複列表。


判斷/條件覆蓋測試

把分支涵蓋率和條件涵蓋率的要求疊起來:每個判斷式的真假都要測到,判斷式裡每個子條件的真假也都要測到。聽起來案例應該要更多,但實際設計數據的時候,常常兩、三個案例就能一次把兩層都顧到,不用先做完一種涵蓋率、再回頭補另一種。

還是拿 M1(x > 3 && z < 10)、M2(x == 4 || y > 5)來看,這次挑兩組讓所有子條件同時翻轉的數字:

圖七:判斷/條件涵蓋率——2 個案例顧到 6 個要求

測試案例 x>3 z<10 M1 x==4 y>5 M2
x=4, y=8, z=5 T T True T T True
x=2, y=2, z=15 F F False F F False

兩個案例,六個要求——M1 的真假、M2 的真假、四個子條件各自的真假——全部一次到位。

判斷/條件涵蓋率有個沒解決的問題:子條件各自測過真假,不代表這個子條件真的「說了算」。如果 x == 4 不管真假,y > 5 的值都恰好讓 M2 得到同一個結果,測試案例可能全部通過,但完全沒證明 x == 4 真的有影響到 M2 的判定——這種情況叫遮罩效應(masking),MC/DC 就是為了解決它而生的。

修訂的條件/分支涵蓋測試(MC/DC)

MC/DC(Modified Condition/Decision Coverage)比判斷/條件涵蓋率還嚴。判斷/條件涵蓋率只要求每個子條件測過真假,MC/DC 還要求證明這個子條件真的「說了算」:固定其他子條件不動,單獨翻動這一個,判斷式的真假結果要跟著翻過去。翻不動,就代表這個子條件根本沒被獨立驗證過,只是剛好跟著別的條件湊出對的答案。

用 M1(x > 3 && z < 10)示範:

圖八:MC/DC——3 個案例各自證明子條件說了算

案例 x > 3 z < 10 M1 說明
1 T T True x=4, z=5
2 F T False x=2, z=5,z<10 不變,翻動 x>3,M1 跟著從 True 變 False,證明 x>3 有影響力
3 T F False x=4, z=15,x>3 不變,翻動 z<10,M1 跟著從 True 變 False,證明 z<10 有影響力

3 個案例就達成 MC/DC,不需要案例 4(F, F)——這跟前面條件涵蓋率需要湊滿 4 種組合不一樣。一般規律是:n 個子條件用 &&|| 接起來,MC/DC 只需要 n + 1 個案例,不是條件組合涵蓋率的 2ⁿ 個。條件越多,這個差距越明顯。

MC/DC 是航太、醫療這類安全關鍵軟體常見的強制要求(例如飛機軟體適航標準 DO-178C 對最高風險等級的軟體,就要求做到 MC/DC),一般商業軟體不見得需要做到這一層,但邏輯特別關鍵、特別容易出人命或出大錢的模組,值得考慮。


路徑涵蓋測試

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

還是同一段流程:3 個語句(S1、S2、S3)、2 個判斷(M1、M2),走法一共有 4 條:

圖九:路徑涵蓋率——同一段流程走出 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)。

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 路徑
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

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

控制流測試

語句、分支、條件、判斷/條件、MC/DC、路徑,這幾種涵蓋率合起來統稱控制流測試(control flow testing)——共同點是都拿程式的控制流程圖(哪個判斷式接哪個語句、哪個分支通往哪裡)當作設計測試案例的地圖,差別只在「地圖要走到多細」:

圖十:六層涵蓋率由鬆到嚴

實務上不會每個專案都往最嚴的方向做到底:一般單元測試多半以分支涵蓋率當基本門檻——.NET 常用 Coverlet、Java 用 JaCoCo、Python 用 coverage.py、C/C++ 用 gcov,這些工具通常會同時報語句涵蓋率和分支涵蓋率兩個數字,CI 上設門檻時分支涵蓋率才是比較不會被灌水的那個。重要模組加碼做到條件涵蓋率或判斷/條件涵蓋率,MC/DC 留給真的出錯會死人、賠大錢的程式碼,路徑涵蓋率則只用在邏輯複雜但函數本身不大的關鍵函數上。涵蓋率數字往上加之前,先想清楚這個模組值不值得付出對應的測試成本。