Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
JChehe opened this issue Dec 14, 2020 · 0 comments
Open

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

JChehe opened this issue Dec 14, 2020 · 0 comments

Comments

@JChehe
Copy link
Owner

JChehe commented Dec 14, 2020

最近为基于 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 的 流程控制语句 有:

  • if
  • while
  • do ... while
  • switch
  • ...

运算符:

  • 三目运算符(condition ? exprIfTrue : exprIfFalse

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

branch

其他标识

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

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

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》

参考资料

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant