JavaScript覆蓋率統計實現

六幻發表於2016-01-08

主要需求

1、 支援browser & nodejs

因為javascript既可以在瀏覽器環境執行,也可以在nodejs環境執行,因此需要能夠統計兩種環境下單元測試的覆蓋率情況。

2、 透明、無縫

使用者寫單元測試用例的時候,不需要為了支援覆蓋率統計多寫程式碼,之前寫的用例無需修改就可以直接統計覆蓋率情況。

原理

javascript覆蓋率的相關文章比較少,下面的圖是通過閱讀開源javascript覆蓋率工具istanbul及開源測試框架Karma的覆蓋率外掛karma-coverage得出的。javascript覆蓋率統計的核心思想是,在原始碼相應的位置注入統計程式碼,當程式碼執行之後,根據統計程式碼統計的資料確定程式執行的路徑,最終生成覆蓋率統計報告。
screenshot

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:
1. 轉換程式碼(呼叫istanbul的Instrumenter介面)
2. 將instrumented code傳送到瀏覽器(*自己實現*)
3. 將包含覆蓋率資訊的執行結果發回server(*自己實現*)
4. 根據返回的覆蓋率資訊生成覆蓋率報告(呼叫istanbul的Reporter介面)


相關文章