JChehe / blog

🌈 原创&翻译 🌈
717 stars 111 forks source link

看懂「测试覆盖率报告」 #49

Open JChehe opened 3 years ago

JChehe commented 3 years ago

最近为基于 Egg.js 的项目编写单元测试用例。写得七七八八后,想了解一下单元测试的覆盖率。由于第一次接触测试覆盖率报告,对其中一些细节存在疑惑。

经查阅资料后,整理出这篇文章,希望能解答大家一些关于测试覆盖率报告的疑问。

注:以下内容是基于 Istanbul 覆盖率引擎。不同覆盖率引擎可能会存在一些差异。

通过 Istanbul 得到的测试覆盖率报告 通过 Istanbul 得到的测试覆盖率报告

四个测量维度

  1. 行覆盖率(line coverage):每个可执行代码行是否都执行了?
  2. 函数覆盖率(function coverage):每个函数是否都调用了?
  3. 分支覆盖率(branch coverage):每个流程控制的各个分支是否都执行了?
  4. 语句覆盖率(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 declaration 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

split_multi_lines

另外,不管嵌套语句横跨多少行,可执行行的数目仅会加 1。

foo(1, bar());  // +1

foo(1,       // +1
    bar());  // +0

nest_multi_line

细心的读者可能会发现,注释 // +1 的那些行,其左侧都是 Nx 或粉色色块(即这两者与底色——灰色不同)。所以 可以不管以上那些概念,通过颜色的不同(非底色——灰色)即可看出哪些是可执行代码行:

import declaration 绿色方框的是 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);

two_line 左图是 2 lines、2 statements,右图是 1 line、2 statements

流程控制

JavaScript 的 流程控制语句 有:

运算符:

我们需要确保流程控制的每个边界情况(即分支)都被执行(覆盖)。

branch

其他标识

测试覆盖率报告出现的标识有:

other_tag

通过注释语法忽略指定代码

代码中的某些分支可能很难,甚至无法测试。故 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 */ {};

comment
通过注释语法,将 funB 的 if 分支排除。故 Branches 由 2/4 变为 2/3,即总分支数由 4 减为 3。

关于 Istanbul 注释语法的更多信息,请查阅《Ignoring code for coverage purposes》

参考资料