blog
                                
                                
                                
                                    blog copied to clipboard
                            
                            
                            
                        看懂「测试覆盖率报告」
最近为基于 Egg.js 的项目编写单元测试用例。写得七七八八后,想了解一下单元测试的覆盖率。由于第一次接触测试覆盖率报告,对其中一些细节存在疑惑。
经查阅资料后,整理出这篇文章,希望能解答大家一些关于测试覆盖率报告的疑问。
注:以下内容是基于 Istanbul 覆盖率引擎。不同覆盖率引擎可能会存在一些差异。
通过 Istanbul 得到的测试覆盖率报告
四个测量维度
- 行覆盖率(line coverage):每个可执行代码行是否都执行了?
 - 函数覆盖率(function coverage):每个函数是否都调用了?
 - 分支覆盖率(branch coverage):每个流程控制的各个分支是否都执行了?
 - 语句覆盖率(statement coverage):每个语句是否都执行了?
 

四个测量维度
理解以上四个测量维度并没什么大问题,但还是有些细节可以深究。
行(Lines of Source Code) vs 可执行代码行(Lines of Executable Code)
“行覆盖率”中的行是指可执行代码行(Lines of Executable Code),而不是源文件中所有的行(含空行)——(Lines of Source Code)。
可执行代码行:
一般来说,包含语句的每一行都应被视为可执行行。而复合语句(简称为语句块,用 {} 括起来)会被忽略(但其内容除外)。
注:对于可执行行的定义,不同覆盖率引擎可能会存在一些差异。
因此:
function doTheThing ()  // +0 
{                       // +0
    const num = 1;      // +1
    console.log(num);   // +1
}                       // +0
具体以下东西会被忽略(即视为非可执行行,+0):
非语句
一些覆盖率引擎会将以下两点视为可执行行,而 Istanbul 会忽略它们:
- 该行只包含标点符号:}、});、;
 - 定义时的方法(函数)名
 
import、声明
import { isEqual } from 'lodash';  // +0
const path = require('path');      // +1
require('jquery')                  // +1
let filePath                  // +0
const fileName = 'a.txt';     // +1  注:不仅是声明,还有赋值
class Person {                // +0
    constructor (name) {      // +0
        this.name = name;     // +1
    }                         // +0
    
    static sayHello () {      // +0
        console.log('hello'); // +1
    }                         // +0
    
    walk () {}                // +0
}                             // +0
function doTheThing ()  // +0 
{                       // +0
    const num = 1;      // +1
    console.log(num);   // +1
}                       // +0
import、声明都被视为非可执行行(+0),require、赋值等语句视为可执行行(+1)
如果某行存在可执行代码,则这一整行会被视为可执行代码行。
而如果一个语句被拆分为多行,则该可执行代码块中,仅第一行被会视为可执行行。
因此:
'use strict';
for         // +1
  (         // +0
   let i=0; // +1
   i < 10;  // +0
   i++      // +0
  )         // +0
{           // +0
}           // +0
console.log({  // +1
    a: 1,      // +0
    b: 2,      // +0
})             // +0
function func () {  // +0
    return {        // +1
        a: 1,       // +0
        b: 2,       // +0
    }               // +0
}                   // +0

另外,不管嵌套语句横跨多少行,可执行行的数目仅会加 1。
foo(1, bar());  // +1
foo(1,       // +1
    bar());  // +0

细心的读者可能会发现,注释 // +1 的那些行,其左侧都是 Nx 或粉色色块(即这两者与底色——灰色不同)。所以
可以不管以上那些概念,通过颜色的不同(非底色——灰色)即可看出哪些是可执行代码行:
绿色方框的是 Lines of Source Code、红色红框内与底色不同的色块是 Lines of Executable Code
关于可执行行的更多信息,可查阅:《sonarqube——Executable Lines》。
可执行代码行 vs 语句
一般情况下,如果我们遵守良好的代码规范,可执行代码行和语句的表现是一致的。然而当我们将两个语句放一行时,就会得到不同的结果。
// 2 lines、2 statements
const x = 1;
console.log(x);
// 1 line、2 statements
const x = 1; console.log(x);
左图是 2 lines、2 statements,右图是 1 line、2 statements
流程控制
JavaScript 的 流程控制语句 有:
- if
 - while
 - do ... while
 - switch
 - ...
 
运算符:
- 三目运算符(
condition ? exprIfTrue : exprIfFalse) 
我们需要确保流程控制的每个边界情况(即分支)都被执行(覆盖)。

其他标识
测试覆盖率报告出现的标识有:
- 'E':'else path not taken',表示 if/else 语句的 
if(含else if)分支已测试,而else分支未测试。 - 'I':'if path not taken',与上面的 'E' 相反,即 
if(含else if) 分支未测试。 - 'Nx':表示当前可执行代码行被执行的总次数。
 - 粉色(背景色):语句/函数未覆盖。
 - 黄色(背景色):分支未覆盖。
 

通过注释语法忽略指定代码
代码中的某些分支可能很难,甚至无法测试。故 Istanbul 提供 注释语法,使得某些代码不计入覆盖率。
// 忽略一个 else 分支
/* istanbul ignore else */
if (foo.hasOwnProperty('bar')) {
    // do something
}
// 忽略一个 if 分支
/* istanbul ignore if */
if (hardToReproduceError)) {
    return callback(hardToReproduceError);
}
// 忽略默认值 {}
var object = parameter || /* istanbul ignore next */ {};

通过注释语法,将 funB 的 if 分支排除。故 Branches 由 2/4 变为 2/3,即总分支数由 4 减为 3。
关于 Istanbul 注释语法的更多信息,请查阅《Ignoring code for coverage purposes》。