“覆盖率检测”的实现原理,就这?

覆盖率检测是用来判断单测完整性的,jest 和 karma 都提供了这种功能:

覆盖率就是执行过的代码占总代码的比例,比如执行了多少行(Line),执行了多少个分支(Branch),执行了多少个函数(Function),执行了多少条语句(Statement)。

用它比上总的数量就是覆盖率,分为行覆盖率、分支覆盖率、函数覆盖率、语句覆盖率等。

看起来是不是很神奇,执行完一遍就能知道覆盖到了哪些代码,其实实现原理比较简单,相信看完这篇文章,你会有“就这?”的感觉。

原理探究

jest 和 karama 都是基于 istanbul 做的覆盖率检测,我们来探究下 istanbul 的实现原理。

测试代码如下:

我们执行 istanbul 的 instrument 命令:

npx istanbul instrument ./test.js -o ./out.js
复制代码

instrument 是指函数插桩,也就是透明的给函数添加一些代码。

为什么要插桩呢?看完生成的代码你就明白了。

我们来格式化一下,把变量名替换下。

这就是转换后的代码,在每一个 statement,每一个 function、每一个 branch 都做了计数,分别是 s、f、b 属性。

上面还有一段代码:

初始化了全局变量 AAA,记录了这些信息:

  • path:路径
  • s:statement 数
  • b:branch 数
  • f:function 数
  • fnMap:function 的开始结束位置信息
  • statementMap:statement 的开始结束位置信息
  • branchMap: branch 的开始结束位置信息

看到这里我们大概就能搞懂覆盖率的原理了,就是对每个 statement、function、branch 都插入一段计数代码,记录在一个全局对象中。

为了不和别的全局变量冲突,这个对象的名字是随机生成的,比如 __cov_5ZoEXQ_Hbo27uXArxdm2oA,这里为了简化改为了 AAA。

我们搞明白了覆盖率就是靠插入计数代码,那怎么做的插桩呢?

函数插桩

函数插桩是基于 AST,找到 statement、function、branch 的 AST,在前面插入插桩代码的 AST。

istanbul 确实也是这么做的。

下面是 istanbul 的源码(只看红线标出的位置就行):

就是通过 esprima(js parser)来把代码 parse 成 AST,然后对 AST 进行插桩。

插桩代码分为两部分,一部分是初始化全局对象的代码,一部分是每个分支、语句、函数的计数代码。

我们分别来看下:

初始化全局对象的代码插桩

istanbul 初始化了全局的 coverState 对象用于统计:

做插桩的时候会记录信息到这个 coverState 中:

最后把 coverState 变成字符串加入到代码里:

那具体的分支、语句、函数的 AST 是怎么插桩的?

分支、语句、函数的插桩

对不同 AST 的插桩,就是遍历过程中根据类型做不同的处理:

然后,具体的插桩就是在前面插入一段 AST:

statement 插桩:

function 插桩:

看到这里,我们就知道了函数插桩的实现原理,就是遍历 AST,在不同的位置插入计数代码的 AST 就可以了。

但是有的同学可能会说了,平时我也没手动生成插桩后的代码啊?用 jest --coverage 跑测试用例自动就做了计数,然后给出覆盖率数据了。

istanbul 是怎么做到透明的插桩的呢?

require hook 实现透明无感知的函数插桩

看过之前一篇 require hook 的魔术那篇文章的小伙伴知道,nodejs 的模块加载是分为 load、extension['.js']、compile 这几步的。

我们只需要重写 extension['.js'] 这一步,就能做到透明的代码转换。

istanbul 也是这么做的:

它就是通过修改了 extension['.js'] 方法,在这里面做了函数插桩,之后执行的代码就是转换过后的了,开发者根本感知不到。

总结

jest 和 karma 都基于 istanbul 实现了覆盖率检测。覆盖率统计的原理就是函数插桩,基于 AST 在代码的 statement、function、branch 处插入计数代码,同时通过 require hook 实现了透明的转换。这样代码一执行就能拿到统计数据,自然就可以算出覆盖率了。

看完之后,是不是觉得:

覆盖率检测的实现,就这?

猜你喜欢

转载自juejin.im/post/7019161146347388959