记得之前在Oracle工作的时候,听过数据库那边同事聊他们的工作流程。他们在修改完代码之后需要跑一个测试用例库,这个用例库有成千上万个用例,通常一晚上可能都跑不完。有时候,一个改动可能会导致数十个用例失败,这样就需要重新修改代码。
上述过程其实就是回归(Regression)测试的概念,任何软件都需要有类似的流程。当我们完成代码修改后,不仅仅要保证新代码可以正常工作,还要保证原有的功能特性不受影响。而完成原有功能的验证工作就是回归测试。
对于上述数据库的开发问题,归根结底是因为数据库的源代码太复杂了,大多数人只能理解非常局部的逻辑。一个改动会牵一发而动全身,影响很多功能特性。正是通过这些用例才保证了所有修改的正确性。但我们需要思考一下,运行的用例到底覆盖哪些代码,有没有覆盖到我们期望的代码,这是如何检测的呢?
本文就介绍一下代码覆盖率检测方面的内容。通过该功能,在运行完用例后,我们可以知道哪些代码被覆盖到了,哪些没有覆盖到,哪些分支覆盖了,覆盖的频度是多少等等。
代码覆盖率工具gcov
代码覆盖率方面有很多商用的工具,但不是我们介绍的范围。本文我们介绍一个开源免费的工具,gcov。其实gcov是GCC内置的一个工具,只有大家安装了GCC就可以使用gcov。虽然gcov是免费的,但使用起来确实非常方便,而且也很有效。
在介绍如何使用gcov之前,我们先简单介绍一下gcov的工作流程。具体如下图所示,核心在编译代码的时候加上“-fprofile-arcs -ftest-coverage”选项。此时会生成一个扩展名为gcno的文件。同时,在编译的程序中会插入一些桩代码来进行统计(这部分内容在后面介绍原理的时候会详细介绍)。
编译完的程序在运行的时候会生成一个扩展名为gcda的文件。最后,在源代码目录下面执行gcov命令就可以生成一个代码覆盖率的报告,其扩展名为gcov。上述gcov命令的完整命令格式如下所示。
gcov main.c
看上去是不是挺简单,但可能还是不够具体。我们举一个简单的例子,看看如何生成代码覆盖率报告。首先我们需要创建一个C或者C++的源代码,这个代码可以非常简单。如下是这个示例程序的完整代码,是不是非常简单!
#include <stdio.h>
int main(int argc, char** argv)
{
int count =0;
while (count < 10) {
printf("Encounter: %d", count);
count ++;
}
if(argc != 1) {
printf("There is more than one arguments!\n");
} else {
printf("There is only one argument!\n");
}
return 0;
}
需要说明的是,为了验证代码覆盖率,我们这里使用了一个条件语句。当执行程序时,有参数和没有参数会走不同的路径。具备上述代码后,我们只需要执行下面命令就可以编译一个可以测试代码覆盖率的程序了。
gcc -fprofile-arcs -ftest-coverage main.c -o gcov_test
上述命令中关键的是gcc的选项 -fprofile-arcs和-ftest-coverage,通过上述选项可以使得gcc在编译程序时进行打桩。编译完成程序后,就可以看到在源代码相同的目录下面多出一个名称为mian.gcno的文件。
接下来我们执行一下这个可执行程序,具体执行方法并没有任何差异,与往常执行程序完全一样。
./gcov_test
程序执行成功后就可以发现在相同的目录下面多出来一个名称为main.gcda的文件。这个其实就是代码覆盖的统计信息。但是你如果想打开看看里面的内容,你会发现完全看不懂。这是因为gcda和gcno都是二进制的格式,需要借助gcov转换为可读模式。
接下来就需要gcov上场了!我们只需要执行如下命令就可以生成代码覆盖率报告,可以看到每行代码的执行情况。
gcov main.c
本例中代码覆盖率报告如下图所示。其中每行前面有数字的表示执行的次数,每行前面的“####”表示这行代码没有被执行到,
-: 0:Source:main.c
-: 0:Graph:main.gcno
-: 0:Data:main.gcda
-: 0:Runs:1
-: 1:#include <stdio.h>
-: 2:
1: 3:int main(int argc, char** argv)
-: 4:{
1: 5: int count =0;
11: 6: while (count < 10) {
10: 7: printf("Encounter: %d", count);
10: 8: count ++;
-: 9: }
1: 10: if(argc != 1) {
#####: 11: printf("There is more than one arguments!\n");
-: 12: } else {
1: 13: printf("There is only one argument!\n");
-: 14: }
-: 15:
1: 16: return 0;
-: 17:}
虽然上面的报告还是比较清晰的,但是当文件数量比较多的情况下,可读性要稍微差一下。假设我们的工程再复杂一些,比如增加加法和减法功能的实现,具体如下图所示。这个时候如果要想获取代码覆盖率需要对每个源文件执行gcov命令。
针对上述工程,我们可以通过如下命令编译出一个能够检测代码覆盖率的可执行文件。具体命令如下所示。编译成功后,每个源代码文件都会生成一个gcno文件。
gcc -fprofile-arcs -ftest-coverage add.c -c
gcc -fprofile-arcs -ftest-coverage sub.c -c
gcc -fprofile-arcs -ftest-coverage main.c -c
gcc -fprofile-arcs -ftest-coverage -o gcov_test main.o add.o sub.o
当程序运行完后,每个源代码文件都会生成一个对应的gcda文件。然后我们可以通过gcov生成每个源文件的代码覆盖率情况。我们可以依次执行如下命令。
gcov main.c
gcov add.c
gcov sub.c
虽然没有什么问题,但是当源文件比较多的时候阅读这些零散的覆盖率报告就显得有点繁琐。有没有方法生成一个更加易于查阅的代码覆盖率报告呢?
基于lcov生成覆盖率报告
关于什么lcov,我们可以参考一下man手册的介绍,可以看到lcov本质上基于gcov的数据,最终生成一个便于浏览的HTML的目录树。
我们可以下图简单介绍一下lcov生成代码覆盖率报告的流程。通过下图可以看到,前面编译和运行程序方面并没有差异,差一点是生成数据后不再需要gcov来生成报告,而是需要通过lcov来生成报告。
这里lcov生成报告分为两步,一步是通过lcov生成汇总信息,文件扩展名为info;后一步是通过genhtml工具汇总信息转换为HTML文件,并按照代码的目录结构组织成目录树。基于上述编译并运行完的结果,我们可以通过下面命令生成汇总信息。
lcov -c -d . -o gcov_test.info --rc lcov_branch_coverage=1
然后通过genhtml命令生成HTML格式的代码覆盖率报告。这里生成报告使用了上一步中生成的汇总信息。
genhtml gcov_test.info --no-prefix --output-directory coverage_report
如下是执行该命令是输出的信息,这里包含一个汇总信息。但最重要的时候会创建一个名称为coverage_report的目录。
对于上述覆盖率报告的目录,我们可以用tree命令了解一些详细信息。可以看出,该目录下面是一堆html文件。文件名称与源代码文件有一个对应的关系。
然后我们可以通过浏览器来查看该报告,其中index.thml是报告的入口网页,具体内容如下图所示。可以看到,这里是一个汇总信息,包含总体覆盖率情况,以及安装代码行和函数的覆盖率情况。
我们可以点击目录进入内部,这里可以看到每个文件的代码覆盖率情况。根据我们的代码实现,我们知道add函数并没有被调用到。因此覆盖率报告中的数据根实际情况是相符的。
上图中每个文件都是一个链接,我们可以点进去看该文件的代码覆盖情况。如下图所示,可以看到没有覆盖的代码行都标红显示了。
本文的内容先到这里,后面我们会从原理层面介绍一下代码覆盖率方面的更多细节。
领取专属 10元无门槛券
私享最新 技术干货