C++代码覆盖率测试:分支、语句与MCDC覆盖率
大家好!今天我们来深入探讨C++代码覆盖率测试,重点关注分支覆盖、语句覆盖和MCDC覆盖。代码覆盖率是衡量测试完整性的重要指标,它能帮助我们评估测试用例是否充分地覆盖了代码的各个部分,从而发现潜在的缺陷。
1. 代码覆盖率概述
代码覆盖率指的是测试用例执行过程中,被执行到的代码占总代码的比例。不同的覆盖率标准关注代码的不同方面,提供了不同层次的测试完整性保证。常见的覆盖率标准包括:
- 语句覆盖(Statement Coverage): 度量程序中每个可执行语句是否被执行到。
- 分支覆盖(Branch Coverage): 度量程序中每个分支(例如
if语句的true和false分支)是否被执行到。 - 条件覆盖(Condition Coverage): 度量程序中每个条件表达式中的每个子条件是否取真和取假。
- 路径覆盖(Path Coverage): 度量程序中所有可能的执行路径是否被执行到。
- 修改条件/判定覆盖(Modified Condition/Decision Coverage, MCDC): 一种更严格的覆盖率标准,主要用于安全关键型应用,它要求每个条件表达式中的每个子条件能够独立地影响判定的结果。
语句覆盖是最基本的覆盖率标准,而MCDC覆盖是最高级别的覆盖率标准。实际应用中,我们需要根据项目的具体需求和风险承受能力选择合适的覆盖率标准。
2. 语句覆盖(Statement Coverage)
语句覆盖是最简单的覆盖率指标。它度量程序中每个可执行语句是否被至少执行一次。
示例代码:
#include <iostream>
int absoluteValue(int x) {
int result;
if (x >= 0) {
result = x; // 语句1
} else {
result = -x; // 语句2
}
return result; // 语句3
}
int main() {
std::cout << "Absolute value of 5: " << absoluteValue(5) << std::endl;
return 0;
}
分析:
为了达到语句覆盖,我们需要确保 语句1、语句2 和 语句3 都被执行到。上面的 main 函数只使用了一个正数 5 作为输入,因此只执行了 语句1 和 语句3。
测试用例:
我们需要添加一个负数作为输入,才能覆盖 语句2。
#include <iostream>
int absoluteValue(int x) {
int result;
if (x >= 0) {
result = x; // 语句1
} else {
result = -x; // 语句2
}
return result; // 语句3
}
int main() {
std::cout << "Absolute value of 5: " << absoluteValue(5) << std::endl;
std::cout << "Absolute value of -5: " << absoluteValue(-5) << std::endl;
return 0;
}
现在,通过输入 5 和 -5,我们已经覆盖了所有语句,达到了 100% 的语句覆盖率。
3. 分支覆盖(Branch Coverage)
分支覆盖比语句覆盖更强。它要求程序中每个分支(例如 if 语句的 true 和 false 分支)都被执行到。
示例代码:
#include <iostream>
bool isPositive(int x) {
if (x > 0) {
return true; // 分支1
} else {
return false; // 分支2
}
}
int main() {
std::cout << "Is 5 positive? " << isPositive(5) << std::endl;
return 0;
}
分析:
为了达到分支覆盖,我们需要确保 if 语句的 true 分支 (分支1) 和 false 分支 (分支2) 都被执行到。上面的 main 函数只使用了一个正数 5 作为输入,因此只执行了 true 分支。
测试用例:
我们需要添加一个非正数(例如 0 或负数)作为输入,才能覆盖 false 分支。
#include <iostream>
bool isPositive(int x) {
if (x > 0) {
return true; // 分支1
} else {
return false; // 分支2
}
}
int main() {
std::cout << "Is 5 positive? " << isPositive(5) << std::endl;
std::cout << "Is -5 positive? " << isPositive(-5) << std::endl;
return 0;
}
现在,通过输入 5 和 -5,我们已经覆盖了所有分支,达到了 100% 的分支覆盖率. 注意,分支覆盖也同时满足了语句覆盖。
4. 修改条件/判定覆盖(Modified Condition/Decision Coverage, MCDC)
MCDC 是一种更严格的覆盖率标准,主要用于安全关键型应用。它要求满足以下两个条件:
- 判定覆盖: 每个判定(decision,例如
if语句的条件表达式)的所有可能结果都至少出现一次。 - 条件覆盖: 每个判定中的每个条件(condition,例如
x > 0中的x)都能够独立地影响判定的结果。
示例代码:
#include <iostream>
bool complicatedCondition(bool a, bool b) {
if (a && b) {
return true;
} else {
return false;
}
}
int main() {
std::cout << "complicatedCondition(true, true): " << complicatedCondition(true, true) << std::endl;
return 0;
}
分析:
这个例子中,判定是 a && b,条件是 a 和 b。为了满足 MCDC 覆盖,我们需要找到一组测试用例,使得:
a && b的结果为true和false都至少出现一次(判定覆盖)。a的值变化时,a && b的结果也变化(b的值保持不变)。b的值变化时,a && b的结果也变化(a的值保持不变)。
测试用例:
| a | b | a && b | a 独立影响 | b 独立影响 |
|---|---|---|---|---|
| true | true | true | ||
| false | true | false | 是 | |
| true | false | false | 是 |
从上表可以看出,我们需要三个测试用例:
a = true, b = truea = false, b = truea = true, b = false
代码实现:
#include <iostream>
bool complicatedCondition(bool a, bool b) {
if (a && b) {
return true;
} else {
return false;
}
}
int main() {
std::cout << "complicatedCondition(true, true): " << complicatedCondition(true, true) << std::endl;
std::cout << "complicatedCondition(false, true): " << complicatedCondition(false, true) << std::endl;
std::cout << "complicatedCondition(true, false): " << complicatedCondition(true, false) << std::endl;
return 0;
}
现在,我们已经通过了 MCDC 覆盖测试。对于更复杂的条件表达式,可能需要更多的测试用例才能满足 MCDC 覆盖。
5. C++ 代码覆盖率工具
有许多工具可以帮助我们测量 C++ 代码的覆盖率。以下是一些常用的工具:
- gcov/lcov: 这是 GCC 编译器自带的覆盖率工具。
gcov负责收集覆盖率数据,lcov负责生成 HTML 格式的报告。 - llvm-cov/llvm-profdata: 这是 LLVM 编译器工具链提供的覆盖率工具。
- BullseyeCoverage: 一款商业覆盖率工具,功能强大,支持多种覆盖率标准。
- Coverity: 一款商业静态分析工具,也可以用来测量代码覆盖率。
使用 gcov/lcov 的示例:
-
编译代码时,添加
-fprofile-arcs -ftest-coverage选项:g++ -fprofile-arcs -ftest-coverage main.cpp -o main -
运行可执行文件:
./main -
使用
gcov生成覆盖率数据:gcov main.cpp这会生成
main.cpp.gcov文件,其中包含了覆盖率信息。 -
使用
lcov生成 HTML 报告:lcov -d . -c -o coverage.info genhtml coverage.info -o coverage_report这会生成一个名为
coverage_report的目录,其中包含了 HTML 格式的覆盖率报告。打开coverage_report/index.html即可查看报告。
代码例子:
// main.cpp
#include <iostream>
int add(int a, int b) {
if (a > 0) {
return a + b;
} else {
return a - b;
}
}
int main() {
std::cout << "add(5, 3): " << add(5, 3) << std::endl;
std::cout << "add(-5, 3): " << add(-5, 3) << std::endl;
return 0;
}
编译,运行,生成覆盖率报告的命令如下:
g++ -fprofile-arcs -ftest-coverage main.cpp -o main
./main
gcov main.cpp
lcov -d . -c -o coverage.info
genhtml coverage.info -o coverage_report
生成的html报告会显示哪些行被执行了,哪些行没有被执行。
6. 在 CI/CD 中集成代码覆盖率
将代码覆盖率集成到 CI/CD 流程中,可以自动化地测量和监控代码覆盖率,确保代码质量。
示例:
- 在 CI/CD 配置文件(例如
.gitlab-ci.yml)中,添加一个步骤来编译代码并生成覆盖率数据。 - 添加一个步骤来生成覆盖率报告,并将其发布到 CI/CD 服务器上。
- 设置一个覆盖率阈值,如果代码覆盖率低于该阈值,则构建失败。
.gitlab-ci.yml 示例:
stages:
- build
- test
- coverage
build:
stage: build
script:
- g++ -fprofile-arcs -ftest-coverage main.cpp -o main
test:
stage: test
script:
- ./main
coverage:
stage: coverage
script:
- gcov main.cpp
- lcov -d . -c -o coverage.info
- genhtml coverage.info -o coverage_report
- python3 -c "import re; cov = open('coverage_report/index.html').read(); percentage = float(re.search(r'(d+.?d*)%', cov).group(1)); assert percentage > 80, f'Coverage is {percentage}%, which is below the threshold of 80%'"
artifacts:
paths:
- coverage_report
这个示例中,coverage 阶段会生成 HTML 格式的覆盖率报告,并检查覆盖率是否高于 80%。如果低于 80%,则构建失败。
7. 代码覆盖率的局限性
虽然代码覆盖率是一个有用的指标,但它也有一些局限性:
- 高覆盖率并不意味着没有错误。 即使代码覆盖率达到 100%,仍然可能存在未被发现的错误,因为覆盖率只能保证代码被执行到,不能保证代码的逻辑正确。
- 代码覆盖率只能发现代码中的缺陷,不能发现需求中的缺陷。
- 代码覆盖率可能会导致过度测试。 为了达到高覆盖率,测试人员可能会编写一些没有实际意义的测试用例,浪费时间和资源。
因此,我们需要将代码覆盖率与其他测试方法结合起来使用,例如单元测试、集成测试、系统测试和用户验收测试,才能更有效地发现和修复缺陷。同时,需要关注代码覆盖率报告,分析未覆盖的代码,思考是否需要添加测试用例。
8. 总结
本文介绍了 C++ 代码覆盖率测试,重点关注了语句覆盖、分支覆盖和 MCDC 覆盖。我们学习了如何使用 gcov/lcov 等工具来测量代码覆盖率,以及如何将代码覆盖率集成到 CI/CD 流程中。代码覆盖率是提高代码质量的重要手段,但需要结合其他测试方法一起使用,才能更有效地发现和修复缺陷。不同的覆盖率标准有不同的侧重点,需要根据实际情况进行选择。
更多IT精英技术系列讲座,到智猿学院