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各自都能独立决定整个判定的真假。

怎么做?看这张图:

MC/DC 核心逻辑:条件独立性 判定: A && B 条件 A 条件 B 测试用例要求: (A=T, B=T) → 结果为真 (A=F, B=T) → 结果为假(证明A独立影响) (A=T, B=F) → 结果为假(证明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