-
Notifications
You must be signed in to change notification settings - Fork 111
Description
最近为基于 Egg.js 的项目编写单元测试用例。写得七七八八后,想了解一下单元测试的覆盖率。由于第一次接触测试覆盖率报告,对其中一些细节存在疑惑。
经查阅资料后,整理出这篇文章,希望能解答大家一些关于测试覆盖率报告的疑问。
注:以下内容是基于 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》。