代码覆盖率是软件测试中用来评估测试用例效果的一个重要指标,表示被测试代码中实际执行的部分与总代码的比率。它能帮助开发者了解哪些代码得到了测试,哪些部分未被覆盖,从而改进测试用例的设计和执行。通过代码覆盖率的分析,开发者可以发现潜在的缺陷和未被测试的代码,从而提高软件质量和测试效率。
覆盖率统计的口径不同,最后统计的覆盖率的数据也不同,一般分为:
代码覆盖率测试虽然能够帮助开发者发现问题,但也并不意味着代码覆盖率没问题就意味着测试用例没问题。覆盖率高并不代表测试用例完全覆盖了所有可能的情况,还需要结合其他测试方法和技术来保证测试的全面性和有效性。比如:
同时,不同场景下的代码覆盖率要求也不同。比如:
市面上不论开源还是商业软件,都有很多代码覆盖率工具,下面主要介绍几种主流的代码覆盖率工具。
工具名称 | 支持语言 | 插桩方式 | 输出格式 | 跨平台 | 测试粒度 | 性能损耗 | 调试支持 |
---|---|---|---|---|---|---|---|
GCOV | C/C++ | 源码插桩 | 文本/HTML | Linux | 行/分支覆盖 | 低 | 一般 |
LLVM Coverage | C/C++/Rust等 | 源码插桩 | HTML/XML | 跨平台 | 边缘覆盖 | 中 | 完善 |
BullseyeCoverage | C/C++/C# | 二进制插桩 | 自定义 | 跨平台 | MC/DC覆盖 | 高 | 完善 |
OpenCppCoverage | C++ | 二进制插桩 | HTML | Windows | 行覆盖 | 中 | 基础 |
字段说明:
代码覆盖率的测量通常通过插桩技术实现,插桩技术主要分为两种类型:
需要注意的是既然是代码插装也就意味着一定对性能有影响,因此覆盖率只能用于功能测试。
不同的工具或者编译器支持的覆盖率测试方案虽然不同,但是其基础原理基本上一致。
比如GCC的gcov在编译时添加 -fprofile-arcs 和 -ftest-coverage 标志。这会在生成的可执行文件中插入代码,以便在运行时记录每个基本块的执行次数。程序执行后gcov 会生成 .gcda 文件,这些文件包含了每个基本块的执行次数。这些数据是后续生成覆盖率报告的基础。
而LLVM 提供了 SanitizerCoverage 选项,用于插桩代码以收集覆盖率信息。通过编译时选项启用后,LLVM 会在生成的代码中插入额外的监控逻辑。SanitizerCoverage 使用 8 位位图来记录控制流图中的边缘覆盖情况。这种方法高效地存储了每条边是否被执行的信息,便于后续分析。覆盖率数据通常以内存映射文件的形式输出,方便在程序运行后进行读取和处理。这样可以有效管理数据并提高性能。
代码是否执行到以及执行的次数需要在运行中进行统计,为了能够统计到相关的数据,LLVM在IR中插入计数代码来统计该数据。对于编译器来说,一段程序是否执行到只需要关注入口和出口即可,如果入口执行了出口也执行了那么中间的代码一定也执行了。因此编译器统计覆盖率的基础待单元是每一个基本块(BB,Basic Block),基本块是指一段连续的代码,具有以下特点:
LLVM基本块生成的步骤为:
其伪代码如下:
function generateBasicBlocks(ast):
blocks = []
currentBlock = createNewBlock()
for statement in ast:
if isJump(statement):
currentBlock.add(statement)
blocks.append(currentBlock)
currentBlock = createNewBlock()
else:
currentBlock.add(statement)
if currentBlock.hasInstructions():
blocks.append(currentBlock)
return blocks
有了基本块,还需要知道控制流图CFG,根据CFG分析不同BB之间执行顺序。控制流图(Control Flow Graph, CFG)是程序中控制流的可视化表示。它由节点和边组成,节点表示基本块,边表示控制流路径。在构建BB时就可以顺势构建出对应的CFG。
覆盖率计数指令的插入通过对每个函数和基本块的遍历,统计后继基本块的数量,并为每个后继基本块创建计数器数组,从而实现对执行情况的精确统计。在插入覆盖率计数指令的过程中,主要步骤如下:
for each function f in compilation_unit:
write_function_info_to_gcno(f)
for each basic_block bb in f:
n = count_successors(bb)
ctr = create_array(n)
for each successor i of bb:
insert_counting_instruction(ctr[i])
LLVM的代码覆盖率使用比较简单,只需要修改编译参数即可,比如这里用下面的代码作为测试代码。要使用启用覆盖率的代码进行编译,向编译器传递 -fprofile-instr-generate -fcoverage-mapping
即可。
std::vector<int> GenerateInt(const int count){
std::vector<int> result;
for (int i = 0; i < count; ++i) {
result.push_back(2);
}
return result;
}
void PrintOddVector(const std::vector<int> &vec){
for (const auto& num : vec) {
if(num % 2 != 0){
std::cout << num << " ";
}else{
std::cout<<"\n";
}
}
}
int main(int argc, char **argv){
auto numbers = GenerateInt(20);
PrintOddVector(numbers);
return 0;
}
之后执行程序,当程序退出时,它将把一个 原始配置文件 写入由 LLVM_PROFILE_FILE
环境变量指定的路径。如果该变量不存在,则配置文件将写入当前目录的default.profraw
。如果LLVM_PROFILE_FILE
包含指向不存在目录的路径,则将创建缺少的目录结构。
在生成覆盖率报告之前,必须对原始配置文件进行 索引。这可以使用 llvm-profdata
中的 “merge” 工具完成(该工具可以合并多个原始配置文件并在同一时间对它们进行索引),最后使用llvm-cov
工具生成报告。
#编译命令
clang++ -fprofile-instr-generate -fcoverage-mapping -std=c++17 main.cpp -o foo
llvm-profdata merge -sparse default.profraw -o main.profdata
#生成报告
llvm-cov report ./foo.exe -instr-profile="main.profdata"
最后通过命令生成报告:
➜ LLVMConverage git:(master) llvm-cov report ./foo.exe -instr-profile="main.profdata"
Filename Regions Missed Regions Cover Functions Missed Functions Executed Lines Missed Lines Cover Branches Missed Branches Cover
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
B:/Code/TestForLearn/Src/LLVMConverage/main.cpp 10 1 90.00% 3 0 100.00% 21 1 95.24% 6 1 83.33%
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TOTAL 10 1 90.00% 3 0 100.00% 21 1 95.24% 6 1 83.33%
llvm-cov
也支持其他参数可以生成更详细的报告(更详细的内容参考llvm文档),比如:
➜ LLVMConverage git:(master) ✗ llvm-cov show ./foo.exe --show-branches=count -instr-profile="main.profdata"
1| |#include
2| |#include
3| |#include
4| |
5| 1|std::vector<int> GenerateInt(const int count){
6| 1| std::vector<int> result;
7| 21| for (int i = 0; i < count; ++i) {
------------------
| Branch (7:21): [True: 20, False: 1]
------------------
8| 20| result.push_back(2);
9| 20| }
10| 1| return result;
11| 1|}
12| |
13| |
14| 1|void PrintOddVector(const std::vector<int> &vec){
15| 20| for (const auto& num : vec) {
------------------
| Branch (15:26): [True: 20, False: 1]
------------------
16| 20| if(num % 2 != 0){
------------------
| Branch (16:12): [True: 0, False: 20]
------------------
17| 0| std::cout << num << " ";
18| 20| }else{
19| 20| std::cout<<"\n";
20| 20| }
21| 20| }
22| 1|}
23| |
24| 1|int main(int argc, char **argv){
25| 1| auto numbers = GenerateInt(20);
26| 1| PrintOddVector(numbers);
27| 1| return 0;
28| 1|}