JavaScript覆蓋率統計實現,javascript覆蓋率
主要需求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 codefunction abs(num){ if(abs > 0) return num; else return -num;}
//instrumented codevar __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進行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介面)
編程實現程式碼涵蓋範圍及路徑分析檢測,C++, C#, Java, JavaScript(任選一種程式語言)
如果你採用源碼插裝的方式來做的話,需要對輸入程式進行詞法,文法分析建立文法分析樹(有一些現成的工具如bison, antlr可以藉助),通過語分析法分析樹擷取程式控制流程關係,然後就可以對程式的基本塊進行分析了。要考慮擷取覆蓋率和程式執行路徑,需要對基本塊做插裝,利用插入的探針來對它們的執行情況做記錄。
javascript統計新聞瀏覽次數的函數
針對一條新聞的瀏覽次數,不如在資料庫對應此條新聞增加一個欄位,用於統計瀏覽次數,初始化為0
每點擊一次這條新聞的連結,就用xmlHttp(我只會這樣。。)非同步訪問後台頁面,在背景程式中更新一下這個欄位,使之+=1,這不就行了麼。