2. 结构覆盖基础概念:语句覆盖、分支覆盖、MC/DC覆盖的定义与区别
各位工程师朋友,咱们今天聊聊结构覆盖。说实话,这玩意儿是DO-178C里最容易被误解、也最容易出坑的地方。我见过不少项目,测试报告写得漂漂亮亮,覆盖率数字也好看,结果适航审查一来,全被打回。为什么?因为没搞懂这三种覆盖到底在测什么。
先给个直观的比喻。你写了一段代码,就像画了一张地图。语句覆盖是检查每条路有没有走过;分支覆盖是检查每个岔路口有没有转过;MC/DC覆盖呢?它要检查每个岔路口的每个路标是不是真的起了作用。嗯,这个比喻不完美,但能帮你快速建立印象。
2.1 语句覆盖:最基础的“走过场”
语句覆盖的定义很简单:程序中每一条可执行语句至少被执行一次。说白了,就是你的测试用例得让每一行代码都跑一遍。
举个例子,看这段C代码:
int calculate(int a, int b) {
int result = 0;
if (a > 5) {
result = a + b; // 语句1
} else {
result = a - b; // 语句2
}
return result; // 语句3
}
要达到100%语句覆盖,你只需要两个测试用例:一个让a>5成立,一个让a>5不成立。这样语句1、2、3都能跑到。
关键点:语句覆盖是最低要求。DO-178C里,Level D的软件才只要求语句覆盖。Level C以上,光靠这个是不够的。
我在项目中遇到过一件事:有个同事信誓旦旦说语句覆盖100%了,结果审查时发现,他漏了一个if-else里的else分支。为什么?因为他写的测试用例全走的是true分支,false分支根本没执行到。他以为语句覆盖就是“所有行都跑过”,但忘了有些行藏在分支里。
个人习惯:我一般先写语句覆盖的用例,把它当作“最低保障”。但心里清楚,这远远不够。
2.2 分支覆盖:检查每个岔路口
分支覆盖,也叫判定覆盖。它的定义是:程序中每个判定的真分支和假分支至少各执行一次。
还是刚才那段代码。if (a > 5) 这个判定,有两个分支:真(a>5)和假(a<=5)。分支覆盖要求这两个分支都走到。
你可能会说:“这不跟语句覆盖一样吗?” 不一样。看这个例子:
void process(int x, int y) {
if (x > 0 && y > 0) { // 判定1
do_something(); // 语句A
}
do_other(); // 语句B
}
语句覆盖:只要x>0 && y>0为真一次,do_something()执行一次,do_other()执行一次,就100%了。但分支覆盖呢?它要求判定1的真假分支都走到。也就是说,你需要一个用例让条件为真,另一个让条件为假。
分支覆盖比语句覆盖更严格。它能发现一些语句覆盖发现不了的问题,比如某个分支的逻辑写错了。
注意:分支覆盖不等于条件覆盖。分支覆盖看的是整个判定的结果(真或假),而不是每个子条件。这个区别很重要,后面MC/DC会用到。
我记得有一次做评审,对方说他们的分支覆盖做到了100%。我一看报告,确实每个if都有真假两个分支。但仔细一查,有个switch-case语句,他们只测了default分支,其他case分支全没测。这算分支覆盖吗?严格来说不算,因为每个case其实也是一个分支。嗯,这里要小心。
2.3 MC/DC覆盖:最严格的“独立性检验”
MC/DC,全称是Modified Condition/Decision Coverage。名字挺长,但核心思想就一句话:每个条件都要独立地影响判定的结果。
什么叫“独立地影响”?就是改变这个条件,其他条件不变,判定的结果会跟着变。
看这个判定:if (A && B)。要达到MC/DC覆盖,你需要证明A和B各自都能独立决定整个判定的真假。
怎么做?看这张图:
对于 A && B,你需要三个测试用例:
- (T, T) → 结果为真
- (F, T) → 结果为假(A从T变F,结果变了,证明A独立)
- (T, F) → 结果为假(B从T变F,结果变了,证明B独立)
对于 A || B,也是三个用例:
- (F, F) → 结果为假
- (T, F) → 结果为真(A从F变T,结果变了)
- (F, T) → 结果为真(B从F变T,结果变了)
核心要点:MC/DC不是要求所有条件组合都测到(那是组合覆盖),而是要求每个条件都能“独立地”翻转结果。说白了,就是证明每个条件都不是摆设。
2.4 三种覆盖的对比
咱们用一张表来对比:
| 覆盖类型 | 定义 | DO-178C要求等级 | 测试用例数量(典型) | 能发现的问题 |
|---|---|---|---|---|
| 语句覆盖 | 每条语句至少执行一次 | Level D | 较少 | 死代码、未执行的语句 |
| 分支覆盖 | 每个判定的真假分支各执行一次 | Level C | 中等 | 分支逻辑错误、遗漏的分支 |
| MC/DC覆盖 | 每个条件独立影响判定结果 | Level A | 较多(但比组合覆盖少) | 条件冗余、逻辑错误、隐藏的依赖 |
你想想看,为什么DO-178C对Level A要求MC/DC?因为Level A是“失效会导致灾难性后果”的软件。比如飞控系统,一个条件判断错了,飞机可能就掉下来了。这时候,你必须证明每个条件都是“有意义的”,不是多余的。
避坑指南:我曾经见过一个团队,为了达到MC/DC覆盖,写了大量冗余的测试用例。结果审查时发现,他们虽然覆盖率数字达标,但很多条件并没有真正独立地影响结果。为什么?因为他们用了错误的测试用例组合。比如对于 A && B,他们用了 (T,T)、(F,F)、(T,F),但 (F,F) 并不能证明A独立影响结果,因为B也变了。所以,写用例时一定要盯着“只变一个条件”这个原则。
2.5 实际项目中的选择
在实际项目中,怎么选?我个人的经验是:
- Level D:语句覆盖就够了。别浪费时间做更细的覆盖。
- Level C:分支覆盖是标配。但我会额外加一些条件覆盖的用例,心里踏实。
- Level B:分支覆盖 + 条件覆盖。有些审查方会要求更严格。
- Level A:MC/DC覆盖,没得商量。而且要注意,MC/DC的用例要跟需求挂钩,不能为了覆盖而覆盖。
嗯,这里有个容易忽略的点:MC/DC覆盖不是万能的。它只能证明每个条件独立影响判定,但证明不了判定本身的逻辑是否正确。比如你把 A && B 写成了 A || B,MC/DC覆盖照样能100%通过,但逻辑已经错了。所以,结构覆盖必须跟需求覆盖结合起来,缺一不可。
重要提醒:不要为了追求覆盖率数字而写无意义的测试用例。我见过有人为了达到MC/DC,把条件拆得特别细,结果测试用例数量爆炸,但真正能发现问题的没几个。记住,覆盖是手段,不是目的。目的是保证软件的正确性和安全性。
最后总结一下:语句覆盖是“走过”,分支覆盖是“走到”,MC/DC覆盖是“走透”。三种覆盖层层递进,要求越来越高,但对应的软件关键等级也越来越高。选哪种,看你的软件要干什么。
公众号:蓝海资料掘金营,微信deep3321