Revisiting JavaScript code coverage

Get into the habit of writing together! This is the 10th day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

Original address: Rethinking JavaScript Test Coverage

This article is from Benjamin Coe , who is the product manager of npm and the core maintainer of yargs and istanbul.

Hello everyone, I'm Xiaoyu, a code farmer. Recently, vitest has been used to replace jest in several projects, and the test coverage has been greatly improved. So when I went to check the information to understand the difference between vitest and jest test coverage, I saw some interesting blog posts. This article first describes the advantages and disadvantages of two methods of compiling buried points and V8-based code coverage, and then describes Benjamin Coe's pursuit of code coverage.

It is now possible to set an environment variable NODE_V8_COVERAGEto point to to output code coverage. The c8 tool can generate beautiful report information by overlaying data.

History of Test Coverage

In JavaScript code, history has been collected through clever hacks. Tools like Istanbul and Blanket collect by parsing JavaScript code and burying points, so that the original logic is not affected. for example:

function foo (a) {
  if (a) {
  // do something with 'a'.
  } else {
  // do something else.
  }
}
复制代码

Rewrite as:

function foo(a) {
  cov_2mofekog2n.f[0]++;
  cov_2mofekog2n.s[0]++;
  if (a) {
    // do something with 'a'.
    cov_2mofekog2n.b[0][0]++;
  } else {
    // do something else.
    cov_2mofekog2n.b[0][1]++;
  }
}
复制代码

cov_2mofekog2n.f[0]++Indicates that the foo function was executed, cov_2mofekog2n.s[0]++that the statement in this function was called, and Indicates thatcov_2mofekog2n.b[0][0]++ the branch was executed. cov_2mofekog2n.b[0][1]++Based on the above data, a report is generated.

The above approach works, but also has some disadvantages:

  • 类似 istanbul 这样的工具需要跟上不断发展的 JavaScript 语言,经常会出现跟不上语言特性不同步的情况,比如这个 issue 就是不同步导致的;
  • 在程序每一行都埋点会显著地影响性能;
  • 不在特意修改代码行为上,部分代码很难被收集,比如 hoist statement counter for class variables

我希望能够有一种更好的方式收集代码覆盖率……

V8 中的代码覆盖率

在 Node.js 支持 ESM 后,istanbul 出现了问题。Bradley 重写了 Node.js 的加载机制去支持 ESM 导致不再支持 require 的钩子,这导致 istanbul 很难检测到 ESM 模块已被加载并对其进行检测。我提出这个问题之后,Bradley 给出了另一个建议:

如果利用 V8 新的内置覆盖功能会怎样?

使用直接内置在 V8 引擎中的覆盖可以解决基于转译的代码覆盖方法所面临的许多缺点。好处是:

  • V8 没有使用计数器来检测源代码,而是将计数器添加到从源代码生成的字节码中。这使得计数器改变程序行为的可能性大大降低;
  • 字节码中引入的计数器不会像在源代码的每一行中注入计数器那样对性能产生负面影响;
  • 一旦新的语言特性被添加到 V8 中,它们就会立即被覆盖。

我开始研究使用 Node.js 的 inspector 模块直接从 V8 收集覆盖率;有一些小问题:

  • inspector 的时间问题使得只能检测函数的覆盖率(无法收集块级语句的覆盖率:if 语句、while 语句、switch 语句);
  • 块覆盖缺少一些功能:|| 表达式,&& 表达式;
  • 让 inspector 启动并运行的步骤过于复杂。您需要启动您的程序,启用 inspector,连接到它,然后转覆盖率报告。

撇开这些挑战不谈,通过 inspector 使用 V8 的覆盖范围感觉很有希望。

证明想法

我联系了 V8 团队的 Jakob Gruber ,就我看到的将 V8 覆盖率与 Node.js 集成的错误联系起来。谷歌的人们也很高兴看到 Node.js 中的覆盖支持,并立即着手解决这个问题。

在与几位 V8 维护人员讨论后,我们确定实际上存在一种启用块级覆盖的机制:

  • 需要使用 --inspect-brk 标志启动程序,以便 inspector 立即终止执行;
  • 需要启用覆盖范围;
  • 需要运行 Runtime.runIfWaitingForDebugger 来启动程序执行;
  • 需要监听事件 Runtime.executionContextDestroyed,此时可以输出覆盖率。

我测试了上述方法,它奏效了!

接下来我问 Jakob 是否可以参与并开始在 V8 中实现一些缺失的覆盖功能。在 V8 团队几个人的耐心帮助下,我实现了对 ||&& 表达式的支持。

此时,我们已经输出了详细的 V8 覆盖率信息,但没有简单的方法来输出人类可读的报告。编写了两个 npm 模块来促进这一点:

  • v8-to-istanbul,它将 V8 格式覆盖输出转换为 istanbul 格式。
  • c8,它将整个 inspector 步骤整合到一个命令中,因此您可以通过简单地运行 c8 node foo.js 来收集覆盖率。

利用这些新库,我们终于能够看到覆盖率报告!

这是一个激动人心的里程碑,但我仍然不满意。原因如下:

  • inspector 步骤继续变得复杂;
  • 根据程序退出的方式,例如,如果 process.exit(0) 被调用,则无法转储覆盖率报告;
  • 我们使用的方法要求我们等待 inspector 启动并通过套接字连接到它;这很慢,感觉不雅。

Node 核心实现

我顿悟了,如果可以将 Node.js 隔离在一个总是丢弃覆盖范围的模式中呢?

  • This means that another process does not need to connect to the inspector session and start coverage tracing;
  • This will allow us to better detect when Node.js shuts down so that we can capture process.exit(0) and process.kill events.

In a conversation with Anna Henningsen , it turns out that the implementation of the Node.js inspector matches my thoughts:

  • Inspector actually always runs in most environments, just without the websocket interface enabled;
  • There is an internal inspector protocol available to interact with the inspector without creating a socket connection.

Excited, I confirmed implementing V8 test coverage as a feature of Node.js itself. Here's what it looks like:

  • In Node.js >=10.10.0 you can now set the environment variable NODE_V8_COVERAGE to a directory, which will cause the V8 coverage report to be output at this location;
  • Tool c8 now simply enables the NODE_V8_COVERAGE environment variable, uses V8 to cover the data, and outputs nice reports.

how to use

Now, you can use Node.js's built-in coverage report by following these steps:

  1. Make sure you have upgraded to Node.js 10.10.0;
  2. Install the c8 tool, which can be used to convert V8 coverage output into a readable report;
  3. Execute your application using c8, eg c8 node foo.js.

Guess you like

Origin juejin.im/post/7084973940069629965
Recommended