覆盖检测用于判断单个测试的完整性。jest和karma都提供了这个功能:覆盖率是执行代码占总代码的比例,比如执行了多少行,分支(Branch)有多少,函数(Function)有多少,语句(Statement)有多少被处决。用它来比较总数就是覆盖率,分为行覆盖率、分支覆盖率、函数覆盖率、语句覆盖率等,看起来是不是很神奇?执行一次就可以知道覆盖了哪些代码。其实实现原理比较简单。相信看完这篇文章后,你会觉得“就这些了?”原理探索jest和karama都是基于istanbul进行覆盖检测。下面我们来探讨一下istanbul的实现原理。测试代码如下:我们执行istanbul的instrument命令:npxistanbulinstrument./test.js-o./out.jsinstrument指的是functioninstrumentation,就是透明的给function增加一些代码。为什么需要插入存根?看完生成的代码你就明白了。让我们格式化它并替换变量名。这是转换后的代码。每一条语句、每一个函数、每一个分支都被统计,它们是s、f、b的属性。上面还有一段代码:初始化全局变量AAA,记录信息:path:路径s:语句号b:分支号f:函数号fnMap:函数起止位置信息statementMap:语句起止结束位置信息branchMap:这里可以看到分支的起止位置信息,我们大概可以理解覆盖的原理,就是为每条语句、函数、分支插入一段计数代码,并记录下来在全局对象中。为了不和其他全局变量冲突,这个对象的名字是随机生成的,比如__cov_5ZoEXQ_Hbo27uXArxdm2oA,为简单起见改为AAA。我们发现覆盖率取决于插入计数代码,那么如何进行插桩呢?Functioninstrumentation函数检测是基于AST,找到statement、function、branch的AST,在前面插入检测代码的AST。伊斯坦布尔也是如此。下面是istanbul的源码(只看红线标出的位置):它使用esprima(js解析器)将代码解析成AST,然后插入AST。检测代码分为两部分,一部分是初始化全局对象的代码,另一部分是每个分支、语句和函数的计数代码。单独来看一下:初始化全局对象istanbul的代码,初始化全局coverState对象,用于统计:在做插桩的时候,会将信息记录在这个coverState中:最后将coverState改成字符串添加到代码:那么具体的分支、语句、函数的AST是如何插入的呢?分支、语句、函数针对不同AST的插桩,就是在遍历过程中根据类型做不同的处理:那么,具体的插桩在前面插入一个AST:语句插桩:函数插桩:看这里,我们就知道了functioninstrumentation的实现原理,即遍历AST,在不同位置插入计数代码的AST。但是有的同学可能会说,我平时插桩后不是手动生成代码吗?使用jest--coverage运行测试用例自动统计然后给出覆盖??数据。伊斯坦布尔如何实现透明仪表?requirehookimplementstransparentandimperceptiblefunctioninstrumentation'.js'],编译这些步骤。我们只需要重写extension['.js']这一步就可以实现透明的代码转换。istanbul做同样的事情:它修改了extension['.js']方法,并在其中插入函数,执行的代码被转换,开发者根本感知不到。总结jest和karma都是基于istanbul实现覆盖检测。覆盖率统计的原理是函数插桩,基于AST在代码的语句、函数、分支插入计数代码,通过requirehook实现透明转换。这样一执行代码就可以得到统计数据,自然就可以计算覆盖率了。看完之后,你是不是觉得:覆盖检测的实现就是这样?
