覆盖率检测是就这用来判断单测完整性的,jest 和 karma 都提供了这种功能: 覆盖率就是覆盖执行过的代码占总代码的比例,比如执行了多少行(Line),率检执行了多少个分支(Branch),实现执行了多少个函数(Function),原理执行了多少条语句(Statement)。就这 用它比上总的覆盖数量就是覆盖率,分为行覆盖率、率检分支覆盖率、实现函数覆盖率、原理语句覆盖率等。就这 看起来是覆盖不是很神奇,执行完一遍就能知道覆盖到了哪些代码,率检其实实现原理比较简单,实现相信看完这篇文章,原理你会有“就这?”的感觉。 jest 和 karama 都是基于 istanbul 做的覆盖率检测,我们来探究下 istanbul 的实现原理。 测试代码如下: 我们执行 istanbul 的 instrument 命令: instrument 是指函数插桩,高防服务器也就是透明的给函数添加一些代码。 为什么要插桩呢?看完生成的代码你就明白了。 我们来格式化一下,把变量名替换下。 这就是转换后的代码,在每一个 statement,每一个 function、每一个 branch 都做了计数,分别是 s、f、b 属性。 上面还有一段代码: 初始化了全局变量 AAA,记录了这些信息: 看到这里我们大概就能搞懂覆盖率的原理了,就是对每个 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 的魔术那篇文章的小伙伴知道,nodejs 的模块加载是分为 load、extension[.js]、compile 这几步的。 我们只需要重写 extension[.js] 这一步,就能做到透明的代码转换。 istanbul 也是这么做的: 它就是通过修改了 extension[.js] 方法,在这里面做了函数插桩,之后执行的代码就是转换过后的了,开发者根本感知不到。 jest 和 karma 都基于 istanbul 实现了覆盖率检测。覆盖率统计的原理就是函数插桩,基于 AST 在代码的 statement、function、branch 处插入计数代码,同时通过 require hook 实现了透明的转换。这样代码一执行就能拿到统计数据,自然就可以算出覆盖率了。 看完之后,是不是觉得: 覆盖率检测的实现,就这?
原理探究
函数插桩
初始化全局对象的云南idc服务商代码插桩
分支、语句、函数的插桩
require hook 实现透明无感知的函数插桩
总结