[TOC]
白箱測試
白箱測試看得到程式碼內部結構,設計案例的依據不是輸入輸出,是「程式碼有沒有被跑到」。跑到的程度分成好幾個層級,由鬆到嚴依序是語句、分支、條件、路徑,中間還有判斷/條件涵蓋率和 MC/DC 這兩個進階變體。層級越嚴,案例數通常也跟著變多,這篇按照嚴格程度一層一層往上疊。
覆蓋率的迷思
先看一個容易被覆蓋率數字誤導的例子:
int foo(int a, int b) {
return a / b;
}
測試案例 foo(10, 5),覆蓋率量出來是 100%——這個函數總共就一行程式碼,這一行也確實執行到了。
這真的沒問題嗎?沒有。b = 0 的情況從頭到尾沒被測過,程式一樣會在那個輸入炸掉,但覆蓋率報表看起來完全正常:
覆蓋率量的是「程式碼有沒有被執行到」,不是「輸入的所有可能情況有沒有被想到」。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 = 1,f(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 = 1、b = 2 是不同行)。這兩個案例剛好也把 a > 0 的真假分支都測到了,分支涵蓋率不需要再多加案例——語句涵蓋率和分支涵蓋率在這裡要求一樣多。
循環語句
迴圈的判斷式是 i <= a。f(0) 執行一次迴圈:第一次檢查 0 <= 0 為真,跑完 i 變 1,再檢查 1 <= 0 為假,跳出迴圈——這一個案例剛好把真假分支都測到了,分支涵蓋率不用多加案例。
不過「迴圈一次都不跑」(例如 f(-1))雖然不是分支涵蓋率硬性要求的,實務上還是建議測。這種邊界常常是 bug 藏的地方,只是這已經不算分支涵蓋率的範圍,是邊界值測試在管的事了。
多條件的語句
f(1)、f(2)、f(3)、f(4) 涵蓋了四個 case 各自的分支,但這個 switch 沒有 default,a 不是 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 > 0、i <= a),沒有可以拆的複合條件,條件涵蓋率在這幾個例子上跟分支涵蓋率要求一樣,沒有新東西可以測。真正用得上條件涵蓋率的,是判斷式裡有 &&/|| 的情況。
複雜的條件
還是用 M1(x > 3 && z < 10)這個判斷式來看。M1 有兩個子條件:x > 3 和 z < 10,條件涵蓋率要求這兩個子條件各自的真假都要測到:
| 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 == 4 和 y > 5 的真假組合,這裡不重複列表。
判斷/條件覆蓋測試
把分支涵蓋率和條件涵蓋率的要求疊起來:每個判斷式的真假都要測到,判斷式裡每個子條件的真假也都要測到。聽起來案例應該要更多,但實際設計數據的時候,常常兩、三個案例就能一次把兩層都顧到,不用先做完一種涵蓋率、再回頭補另一種。
還是拿 M1(x > 3 && z < 10)、M2(x == 4 || y > 5)來看,這次挑兩組讓所有子條件同時翻轉的數字:
| 測試案例 | 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)示範:
| 案例 | 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 條:
| 路徑 | 走法 | 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 留給真的出錯會死人、賠大錢的程式碼,路徑涵蓋率則只用在邏輯複雜但函數本身不大的關鍵函數上。涵蓋率數字往上加之前,先想清楚這個模組值不值得付出對應的測試成本。