为了保证代码质量,一般会要求提交的源码要有测试用例覆盖,并对测试覆盖率有一定的要求,在实践中不仅会考核存量代码覆盖率(总体覆盖率)还会考核增量代码的覆盖率。
或者说增量覆盖率更有实际意义,测试用例要随源码一并提交,实时保证源码的质量,而不是代码先行,测试用例后补,这有些应付的意思。
对于存量代码覆盖率主流的测试工具(框架)都是默认支持的,配置reporter相关参数,执行完测试用例就会生成测试报告。
对于增量测试覆盖率主流的测试工具一般没有支持,我想计算增量代码貌似不是测试工具该干的事,所以主流测试工具并没有提供这一功能。
那么如果计算增量覆盖率呢?
计算增量测试覆盖率,总共需要3步:
·计算出增量代码的所有行号
·计算出测试未覆盖的代码的所有行号
·对比计算增量代码被测试覆盖的比例,得出增量覆盖率
是不是很简单,有没有一种 “道理我都懂,就是过不好这一生的赶脚”
一、计算增量代码的所有行号
代码管理一般都会用到 GIT 这个工具,GIT提供了非常强大的管理增量代码的能力,因此,可以利用GIT这一特性,通过git diff(参考文献1) 这个命令获取增量代码。
git diff命令可以使用如下格式,用来对比不同commit(或分支)间的增量代码
git diff []
其中可以是分支名,对比分支间的差异,则是 git diff [] targetBranchName sourceBranchName。可以简写为 git diff targetBranchName 表示对比当前分支与目标分支间的代码增量差异。
例如 git diff master 生成当前分支与master分支的增量信息,当有多个文件变化时,会有多个这样的信息块。
·第1部分是发生变化的文件名。---表示文件发生了删除行 +++表示文件发生了新增的行,当---和+++后面是文件路径(相对代码根目录的相对路径)。
·如果某个文件是新增文件,则---后面是/dev/nul
·如果某个文件被删除了,则+++后面是/dev/nul
·如果文件发生修改,则---和+++后面都有文件名
·先介绍第3部分,因为第2部分的解读需要用第3部分辅助。第3部分是详细的含有上下文的增量信息(增量不是指增加,删除也算增量)
- 表示这一行被删除
+ 表示这一行是新增
如果某一行发生修改,则由一条-和一条+表示
·第2部分是变化的行号信息,以 @@开头和结尾,中间是删除的行号信息和新增的行号信息,以上图为例
-1,11表示,文件出现删除,从第1行开始包含上下文信息一共有11行,在第3部分中分别是第6, 8, 9, 10, 12, 13, 14, 15, 16, 17, 25 行,共11行
+1,18表示,文件出现新增,从第1行是包含上线文信息一共18行,在第3部分中分别是第7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, 25 行,共18行
其中,第8, 9, 10, 12, 13, 14, 15, 16, 25 行是上下文信息,真正删除的行是第6, 17 行,共2行; 新增的行是第7, 11, 18, 19, 20, 21, 22, 23, 24 行,共9行。
不难发现,git diff 默认给出的行号信息,不仅包含真正删除和增加的行,还包含一定的上下文信息(为的是给人看时,能看出到底改了哪些行信息,尤其在一个文件有很多相似或重复的语句的情况下)。并且在计算删除的行的行数时(-1,11中的11)要过滤掉增加的行后再计算,反之亦然(+1,18中的18)。
通过上面的命令确实能计算出增量代码的实际行号(有开始行号,有行数,有差异信息),但对于第3部分的差异信息的解析存在一定的难度,不仅要过滤掉对向信息,还要过滤掉上下文信息。
经查阅文档,发现git diff有一个options是--unified=,简写-U。使用此参数来决定diff结果中上下文信息显示n行,而不是默认的3行。
使用 git diff --unified=0 master 或 git diff -U0 master看运行结果:
数据结构与不带options的结果基本一致,只不过第2部分和第3部分作为一个整体可能会出现1次或多次,还有一点变化是第2部分行号信息的表达出现了三种格式。
-(+)后面只有一个数字,数字是,表示删除(增加)了1行,行号是。此例中-1, +1, +5, -10分别表示第1行删除1行,第1行增加1行,第5行增加1行,第10行删除一行。
-(+)后面有两个数字,第一个数字是,第二个数字是0, 表示删除(增加)了0行,即m行没有变化,此例中-4,0表示第4行没有变化
-(+)后面有两个数字,第一个数字是,第二个数字是,不是0,表示删除(增加了)n行,起始行号是m。此例中+11,7表示从第11行开始,共增加了7行,行号一次递增,即 11, 12, 13, 14, 15, 16, 17 这几行。
因此,计算增量代码的信息只使用第1部分和第2部分就可以完成,由第1部分计算出增量代码的路径,由第2部分的+后面的数字计算得到增量代码的行号(-后面是删除的行信息,不是增量代码)。本例中a.js文件的增量行号是[1, 5, 11, 12, 13, 14, 15, 16, 17]。
由于git diff生成的是固定格式纯文本,解析增量信息时可以按行读取字符串后做正则解析即可。对于linux系统,可以通过管道符|将diff文本导给grep命令(参考文献2),使用正则匹配出需要的信息,命令如下:
git diff -U0 master | grep -Po '^\+\+\+ ./\K.*|^@@ -[0-9]+(,[0-9]+)? \+\K[0-9]+(,[0-9]+)?(?= @@)'
生成结果如下图,此时,再按行遍历,生成以文件路径为Key,增量行号组成的Array为值的Hash表,用于后续逻辑的索引。
二、计算测试未覆盖的代码的所有行号
计算未被测试覆盖的行号,需要先在当前分支运行测试脚本生成对应的测试报告。
测试报告有很多种格式,其中http://lcov.info(参考文献3)是一种描述源码覆盖率的纯文本格式的文件,因此它非常便于计算,可利用此文件计算得到未被覆盖的行号。
http://lcov.info文件内容如下图:
数据包含以下字段,因工具不同,字段出现的顺序会略有变化
·TN: 用例名称,[因工具不同,有的无法生成此字段]
·SF: 源文件路径,[因工具不同,有的是绝对路径,有的是相对路径]
·FN:, 函数起始行号,函数名称,[因工具不同,有的函数名无法生成]
·FNDA:, 函数被执行次数,函数名称,[因工具不同,有的函数名无法生成]
·FNF: 识别统计到的函数数量
·FNH: 被测试覆盖的函数数量, FNH / FHF即函数覆盖率
·BRDA:,,, 条件分支所在行号,块号,分支号,被执行的次数
·BRF: 识别统计到的条件分支数量
·BRH: 被测试覆盖的条件分支数量 BRH / BRF 即分支覆盖率
·DA:,[,] 行号,执行次数, 检验和,[因工具不同,有的有校验和,有的没有]
·LH: 被测试覆盖的行数量
·LF: 可被执行的行数量, LH / LF 即行覆盖率
·end_of_record 统计信息块结束符,一个文件一个块
由此可见,计算未覆盖代码的行号,只需要提取覆盖率数据中SF和DA字段的值即可
·SF是源码文件路径
·DA字段有两个数字,第1个是行号,第2个是执行次数,半角逗号分隔,执行次数的值是0的即是未被覆盖的行
同解析diff增量数据一样,解析覆盖率数据时也可以按行读取字符串后做正则解析即可。对于linux系统,可以通过管道符|连接cat和grep命令(参考文献2),使用正则匹配出需要的信息,命令如下
cat coverage/lcov.info | grep -Po '^SF:\K.*|^DA:\K[0-9]+(?=,0)'
生成的结果如下图,得到未被覆盖的行号,再按行遍历,生成以文件路径为Key,增量行号组成的Set为值的Hash表,用于后续逻辑的索引:
三、最后一哆嗦
得到上面两份数据,就可以计算得出每个文件的增量覆盖率和总体增量覆盖率了。
但还需要考虑一种情况:由于一些原因(可是配置文件的问题)导致一些源码文件未被统计到测试覆盖率报告中,那么 + 有意为之,则增量文件不用计入增量覆盖率中,此文件的增量覆盖率是 100% + 无意为之,则增加文件需要计入增量覆盖率中,此文件的增量覆盖率是 0
伪代码如下:
const incData = { // 增量代码行号Hash表
'path/a.js': [1, 2, 3],
'path/b.js': [2, 3, 4],
...
}
const notCovData = { // 未覆盖代码行号的Hash表
'path/b.js': 'Set(3) {1, 2, 3}',
'path/c.js': 'Set(3) {1, 2, 3}',
...
}
let notCovLintCount = 0
let lineCount = 0
forEach(incData, (data, file) => {
const notCovSet = notCovData.get(file)
const notCovLines = []
if (notCovSet) { // 如果增量代码文件中有未覆盖的行数
forEach(data, lineNum => {
if (notCovSet.has(lineNum)) {
notCovLines.push(lineNum)
}
})
} else { // 增量代码的文件没有被测试覆盖到
if (!ignore) { // 如果是无意为之,所有行号均被统计
notCovLines = notCovLine.concat(data)
}
}
console.log(file, '增量覆盖率:', (1 - notCovLines.length / data.length).toFixed(2) + '%')
lineCount += data.length
notCovLineCount += notCovLines.length
})
console.log('总体增量覆盖率:', (1 - notCovLintCount / lineCount).toFixed(2) + '%')
至此,分支间的增量代码的测试覆盖率计算完成。
详细的实现逻辑可参考 nodejs版本
实践与应用
·一般会用于CI检测中,在test step后添加增量覆盖率检测脚本,增量覆盖率未达标的代码禁止并入代码库
·也可用于git hook中做检测(这会增加提交代码的等待时长,不太建议),增量覆盖率未达标的代码禁止提交。
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理