主要需求
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;elsereturn -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接口)