主要需求
1、 支持browser & nodejs
由于javascript既能够在浏览器环境执行,也能够在nodejs环境执行,因此须要能够统计两种环境下单元測试的覆盖率情况。
2、 透明、无缝
用户写单元測试用例的时候,不须要为了支持覆盖率统计多写代码,之前写的用例无需改动就能够直接统计覆盖率情况。
原理
javascript覆盖率的相关文章比較少。以下的图是通过阅读开源javascript覆盖率工具istanbul及开源測试框架Karma的覆盖率插件karma-coverage得出的。
javascript覆盖率统计的核心思想是,在源码对应的位置注入统计代码,当代码执行之后,依据统计代码统计的数据确定程序执行的路径,终于生成覆盖率统计报告。
1. 转换(instrument)
- 使用开源工具Esprima对源码进行语法分析生成语法树
- 在语法树对应的位置注入统计代码。在程序运行到这个位置的时候对对应的全局变量赋值,确保运行之后可以依据全局变量知道代码的运行流程
- 使用开源工具Escodegen依据注入之后的语法树生成对应的javascript代码,即转换之后的代码(instrumented code)
注:这里进行语法分析的优点是,针对书写不规范的代码(比方一行多个语句),依旧可以非常好统计出分支覆盖和组合覆盖等信息。
2. 运行(run)
这一步须要先加载转换后的代码:
- nodejs:直接通过对
require
语句进行hook来无缝实现,后面会具体介绍 - 浏览器环境:须要将转换后的代码传给浏览器。假设是karma之类的带server的測试框架,须要通过socket传输至浏览量器,运行完之后再将包括覆盖率信息的运行结果传回server。生成測试报告
然后运行单元測试。产生的统计信息会挂在全局变量this
以下。
对于浏览器环境,this
就是window
,而对于nodejs环境this
就是global
。
3. 生成报告(report)
这一步会依据全局标量中的覆盖率信息生成特定格式的报告,如html、lcov、cobertura、teamcity等。
一个样例
//source code
function abs(num){
if(abs > 0)
return num;
else
return -num;
}
//instrumented code
var __cov_iypKC$dWI6uJFmvxThycaA = (Function(‘return this‘))();
if (!__cov_iypKC$dWI6uJFmvxThycaA.__coverage__) { __cov_iypKC$dWI6uJFmvxThycaA.__coverage__ = {}; }
__cov_iypKC$dWI6uJFmvxThycaA = __cov_iypKC$dWI6uJFmvxThycaA.__coverage__;
if (!(__cov_iypKC$dWI6uJFmvxThycaA[‘/Users/lonfee88/Codes/testframe/coverage-jasmine-istanbul-karma/abs.js‘])) {
__cov_iypKC$dWI6uJFmvxThycaA[‘/Users/lonfee88/Codes/testframe/coverage-jasmine-istanbul-karma/abs.js‘] = {"path":"/Users/lonfee88/Codes/testframe/coverage-jasmine-istanbul-karma/abs.js","s":{"1":1,"2":0,"3":0,"4":0},"b":{"1":[0,0]},"f":{"1":0},"fnMap":{"1":{"name":"abs","line":1,"loc":{"start":{"line":1,"column":-15},"end":{"line":1,"column":17}}}},"statementMap":{"1":{"start":{"line":1,"column":-15},"end":{"line":6,"column":1}},"2":{"start":{"line":2,"column":1},"end":{"line":5,"column":14}},"3":{"start":{"line":3,"column":2},"end":{"line":3,"column":13}},"4":{"start":{"line":5,"column":2},"end":{"line":5,"column":14}}},"branchMap":{"1":{"line":2,"type":"if","locations":[{"start":{"line":2,"column":1},"end":{"line":2,"column":1}},{"start":{"line":2,"column":1},"end":{"line":2,"column":1}}]}}};
}
__cov_iypKC$dWI6uJFmvxThycaA = __cov_iypKC$dWI6uJFmvxThycaA[‘/Users/lonfee88/Codes/testframe/coverage-jasmine-istanbul-karma/abs.js‘];
function abs(num){__cov_iypKC$dWI6uJFmvxThycaA.f[‘1‘]++;__cov_iypKC$dWI6uJFmvxThycaA.s[‘2‘]++;if(abs>0){__cov_iypKC$dWI6uJFmvxThycaA.b[‘1‘][0]++;__cov_iypKC$dWI6uJFmvxThycaA.s[‘3‘]++;return num;}else{__cov_iypKC$dWI6uJFmvxThycaA.b[‘1‘][1]++;__cov_iypKC$dWI6uJFmvxThycaA.s[‘4‘]++;return-num;}}
node.js集成覆盖率
通过hook能够直接无缝的载入转换后的代码。能够对以下两种语句进行hook:
require
vm.createScript
对require进行hook的代码是通过对Module._extensions[‘.js‘]
进行赋值实现的:
function hookRequire(matcher, transformer, options) {
options = options || {};
var fn = transformFn(matcher, transformer, options.verbose),
postLoadHook = options.postLoadHook &&
typeof options.postLoadHook === ‘function‘ ? options.postLoadHook : null;
Module._extensions[‘.js‘] = function (module, filename) {
var ret = fn(fs.readFileSync(filename, ‘utf8‘), filename);
if (ret.changed) {//加载instrument之后的代码并执行
module._compile(ret.code, filename);
} else {//加载原来的代码并执行
originalLoader(module, filename);
}
if (postLoadHook) {
postLoadHook(filename);
}
};
}
hook使覆盖率的集成变得简单。甚至不须要写代码,比方Mocha的覆盖率集成,仅仅须要改用例如以下的调用方式就可以:
istanbul cover _mocha -- -R spec test/spec
浏览器集成覆盖率
浏览器集成覆盖率就略微麻烦一点。好在istanbul提供了API:
- 转换代码(调用istanbul的Instrumenter接口)
- 将instrumented code发送到浏览器(自己实现)
- 将包括覆盖率信息的运行结果发回server(自己实现)
- 依据返回的覆盖率信息生成覆盖率报告(调用istanbul的Reporter接口)
时间: 2024-10-09 07:48:46