1 简介
代码覆盖率是软件测试中用来评估测试用例效果的一个重要指标,表示被测试代码中实际执行的部分与总代码的比率。它能帮助开发者了解哪些代码得到了测试,哪些部分未被覆盖,从而改进测试用例的设计和执行。通过代码覆盖率的分析,开发者可以发现潜在的缺陷和未被测试的代码,从而提高软件质量和测试效率。
覆盖率统计的口径不同,最后统计的覆盖率的数据也不同,一般分为:
- 语句覆盖: 计算执行的语句数与总代码行数的比率。
- 分支覆盖: 计算执行的分支数量(如条件判断中的真和假)与总分支数量的比率。
- 路径覆盖: 计算被测试的执行路径与所有可能路径的比率,通常较为复杂。
- 条件组合覆盖: 计算执行的条件组合与所有可能条件组合的比率。
- 函数覆盖: 计算执行的函数数量与总函数数量的比率。
代码覆盖率测试虽然能够帮助开发者发现问题,但也并不意味着代码覆盖率没问题就意味着测试用例没问题。覆盖率高并不代表测试用例完全覆盖了所有可能的情况,还需要结合其他测试方法和技术来保证测试的全面性和有效性。比如:
- 边界条件覆盖:测试输入的边界值,如最大值、最小值、0、空值等;
- 异常处理路径:验证异常情况的处理;
- 参数组合测试:测试不同参数组合的情况;
- 断言有效性:确保测试用例中的断言条件得到满足。
同时,不同场景下的代码覆盖率要求也不同。比如:
- 单元测试:关注代码的基本执行路径,通常使用分支覆盖(Branch Coverage);
- 集成测试:关注模块间的交互路径,使用路径覆盖(Path Coverage);
- 系统测试:关注整个系统的功能,使用行覆盖(Line Coverage)。
2 主流工具
市面上不论开源还是商业软件,都有很多代码覆盖率工具,下面主要介绍几种主流的代码覆盖率工具。
- GCOV:gcov 是 Linux 下 GCC 自带的一个 C/C++ 代码覆盖率分析工具,它可以在编译时插入计数器,在程序运行时记录代码的执行情况,并生成代码覆盖率报告;
- LLVM Coverage:LLVM 是一个开源的编译器基础设施项目,它提供了一套用于构建编译器和工具链的框架。LLVM 提供了一个名为 LLVM Coverage 的功能,用于在编译时插入计数器,在程序运行时记录代码的执行情况,并生成代码覆盖率报告;
- BullseyeCoverage:BullseyeCoverage 是一个商业的代码覆盖率分析工具,它可以在编译时插入计数器,在程序运行时记录代码的执行情况,并生成代码覆盖率报告;
- OpenCppCoverage:OpenCppCoverage 是一个开源的 C++ 代码覆盖率分析工具,它可以在编译时插入计数器,在程序运行时记录代码的执行情况,并生成代码覆盖率报告;
| 工具名称 | 支持语言 | 插桩方式 | 输出格式 | 跨平台 | 测试粒度 | 性能损耗 | 调试支持 |
|---|---|---|---|---|---|---|---|
| GCOV | C/C++ | 源码插桩 | 文本/HTML | Linux | 行/分支覆盖 | 低 | 一般 |
| LLVM Coverage | C/C++/Rust等 | 源码插桩 | HTML/XML | 跨平台 | 边缘覆盖 | 中 | 完善 |
| BullseyeCoverage | C/C++/C# | 二进制插桩 | 自定义 | 跨平台 | MC/DC覆盖 | 高 | 完善 |
| OpenCppCoverage | C++ | 二进制插桩 | HTML | Windows | 行覆盖 | 中 | 基础 |
字段说明:
- 测试粒度:行覆盖(基础路径)、分支覆盖(条件判断)、边缘覆盖(LLVM)、MC/DC(条件组合覆盖)
- 性能损耗:低(<5%)、中(5-20%)、高(>20%)
- 调试支持:基础(崩溃报告)、一般(堆栈跟踪)、完善(可视化调试)
3 代码覆盖率原理
3.1 插桩
代码覆盖率的测量通常通过插桩技术实现,插桩技术主要分为两种类型:
- 源码插桩:在源代码中插入额外的代码(称为插桩代码),以记录每行或每个基本块的执行情况。这种方法保持了源代码的可读性,生成的报告能够与源代码直接对应。典型的工具比如gcov;
- 二进制插桩:在编译后的二进制代码中插入额外的代码,以记录每行或每个基本块的执行情况。这种方法通常需要对二进制代码进行修改,可能会对性能产生影响。典型的工具比如LLVM Coverage。
需要注意的是既然是代码插装也就意味着一定对性能有影响,因此覆盖率只能用于功能测试。
不同的工具或者编译器支持的覆盖率测试方案虽然不同,但是其基础原理基本上一致。
比如GCC的gcov在编译时添加 -fprofile-arcs 和 -ftest-coverage 标志。这会在生成的可执行文件中插入代码,以便在运行时记录每个基本块的执行次数。程序执行后gcov 会生成 .gcda 文件,这些文件包含了每个基本块的执行次数。这些数据是后续生成覆盖率报告的基础。
而LLVM 提供了 SanitizerCoverage 选项,用于插桩代码以收集覆盖率信息。通过编译时选项启用后,LLVM 会在生成的代码中插入额外的监控逻辑。SanitizerCoverage 使用 8 位位图来记录控制流图中的边缘覆盖情况。这种方法高效地存储了每条边是否被执行的信息,便于后续分析。覆盖率数据通常以内存映射文件的形式输出,方便在程序运行后进行读取和处理。这样可以有效管理数据并提高性能。
3.2 LLVM实现
代码是否执行到以及执行的次数需要在运行中进行统计,为了能够统计到相关的数据,LLVM在IR中插入计数代码来统计该数据。对于编译器来说,一段程序是否执行到只需要关注入口和出口即可,如果入口执行了出口也执行了那么中间的代码一定也执行了。因此编译器统计覆盖率的基础待单元是每一个基本块(BB,Basic Block),基本块是指一段连续的代码,具有以下特点:
- 只有一个入口点(第一个指令)。
- 没有跳转到块内部的指令。
- 可能有多个出口点(跳转到其他基本块的指令)。
LLVM基本块生成的步骤为:
- 词法分析:将源代码分解为标记(tokens),识别语法结构。
- 语法分析:根据语法规则构建抽象语法树(AST),表示程序的结构。
- 控制流图(CFG)构建:在生成基本块之前,首先构建控制流图。CFG 显示程序的控制流路径,包括基本块;
- 基本块划分:遍历 AST,根据控制流结构和语句的跳转关系来划分基本块。主要步骤包括:
- 标识入口: 每个基本块的入口是第一个指令。
- 确定出口: 识别条件语句、循环和跳转指令,确定基本块的出口。
- 合并块: 对于连续的无条件跳转的基本块,可以合并,以减少冗余。
- 生成 LLVM IR:对于每个基本块,生成相应的 LLVM IR 指令,表示该块中的计算和控制流。
其伪代码如下:
function generateBasicBlocks(ast):
blocks = []
currentBlock = createNewBlock()
for statement in ast:
if isJump(statement):
currentBlock.add(statement)


9314

被折叠的 条评论
为什么被折叠?



