如何计算增量测试覆盖率

发表于:2020-11-04 09:36

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:还没想好    来源:知乎

  为了保证代码质量,一般会要求提交的源码要有测试用例覆盖,并对测试覆盖率有一定的要求,在实践中不仅会考核存量代码覆盖率(总体覆盖率)还会考核增量代码的覆盖率。
  或者说增量覆盖率更有实际意义,测试用例要随源码一并提交,实时保证源码的质量,而不是代码先行,测试用例后补,这有些应付的意思。
  对于存量代码覆盖率主流的测试工具(框架)都是默认支持的,配置reporter相关参数,执行完测试用例就会生成测试报告。
  对于增量测试覆盖率主流的测试工具一般没有支持,我想计算增量代码貌似不是测试工具该干的事,所以主流测试工具并没有提供这一功能。
  那么如果计算增量覆盖率呢?
  计算增量测试覆盖率,总共需要3步:
  ·计算出增量代码的所有行号
  ·计算出测试未覆盖的代码的所有行号
  ·对比计算增量代码被测试覆盖的比例,得出增量覆盖率
  是不是很简单,有没有一种 “道理我都懂,就是过不好这一生的赶脚”
  一、计算增量代码的所有行号
  代码管理一般都会用到 GIT 这个工具,GIT提供了非常强大的管理增量代码的能力,因此,可以利用GIT这一特性,通过git diff(参考文献1) 这个命令获取增量代码。
  git diff命令可以使用如下格式,用来对比不同commit(或分支)间的增量代码
  git diff [<options>] <commit> <commit>
  其中<commit>可以是分支名,对比分支间的差异,则是 git diff [<options>] 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=<n>,简写-U<n>。使用此参数来决定diff结果中上下文信息显示n行,而不是默认的3行。
  使用 git diff --unified=0 master 或 git diff -U0 master看运行结果:
  数据结构与不带options的结果基本一致,只不过第2部分和第3部分作为一个整体可能会出现1次或多次,还有一点变化是第2部分行号信息的表达出现了三种格式。
  -(+)后面只有一个数字,数字是<m>,表示删除(增加)了1行,行号是<m>。此例中-1, +1, +5, -10分别表示第1行删除1行,第1行增加1行,第5行增加1行,第10行删除一行。
  -(+)后面有两个数字,第一个数字是<m>,第二个数字是0, 表示删除(增加)了0行,即m行没有变化,此例中-4,0表示第4行没有变化
  -(+)后面有两个数字,第一个数字是<m>,第二个数字是<n>,不是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:<test name> 用例名称,[因工具不同,有的无法生成此字段]
  ·SF:<path of the source file> 源文件路径,[因工具不同,有的是绝对路径,有的是相对路径]
  ·FN:<line number of function start>,<function name> 函数起始行号,函数名称,[因工具不同,有的函数名无法生成]
  ·FNDA:<execution count>,<function name> 函数被执行次数,函数名称,[因工具不同,有的函数名无法生成]
  ·FNF:<number of functions found> 识别统计到的函数数量
  ·FNH:<number of function hit> 被测试覆盖的函数数量, FNH / FHF即函数覆盖率
  ·BRDA:<line number>,<block number>,<branch number>,<taken> 条件分支所在行号,块号,分支号,被执行的次数
  ·BRF:<number of branches found> 识别统计到的条件分支数量
  ·BRH:<number of branches hit> 被测试覆盖的条件分支数量 BRH / BRF 即分支覆盖率
  ·DA:<line number>,<execution count>[,<checksum>] 行号,执行次数, 检验和,[因工具不同,有的有校验和,有的没有]
  ·LH:<number of lines with a non-zero execution count> 被测试覆盖的行数量
  ·LF:<number of instrumented lines> 可被执行的行数量, 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),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号