伊斯坦布爾測試覆蓋率的實現原理

原文:伊斯坦布爾測試覆蓋率的實現原理 | AlloyTeam
做者:TAT.STEPHhtml

JavaScript 單元測試現在對於前端開發來講並不陌生,前端工程化以後項目的代碼質量愈來愈受到重視,單元測試無疑是一種衡量代碼質量的重要手段,而測試覆蓋率則是衡量測試完整性的一種手段:經過已執行代碼的覆蓋率,用於評測代碼的可靠性和穩定性,能夠及時發現沒有被測試用例執行到的代碼塊,提早發現可能的邏輯錯誤。前端

伊斯坦布爾(如下簡稱 Istanbul)是一個基於 JavaScript 的測試覆蓋率統計工具,目前絕大多數測試框架好比 jest mocha 等都是使用 Istanbul 來統計覆蓋率的。伊斯坦布爾有一個比較老的版本 istanbul.js(已再也不維護)和一個新的版本 nyc。雖然使用 Istanbul 的人不少,可是幾乎沒有介紹其實現原理的文章,那麼 Istanbul 計算和統計測試覆蓋率的整個流程是怎樣的呢?node

在剖析源碼以前,咱們首先須要瞭解衡量測試覆蓋率的四個維度: git

p1.png
覆蓋率維度

  • Statements: 語句覆蓋率,全部語句的執行率;
  • Branches: 分支覆蓋率,全部代碼分支如 if、三目運算的執行率;
  • Functions: 函數覆蓋率,全部函數的被調用率;
  • Lines: 行覆蓋率,全部有效代碼行的執行率,和語句相似,可是計算方式略有差異;

以上四個指標維度就是 Istanbul 最終要輸出的結果,能夠看出 Istanbul 的核心任務就是實現對這四個指標的計數器,它的內部實現流程大體能夠分爲如下三個步驟:github

第一步:構造源代碼裝飾器

「裝飾器」源碼裏面稱爲 instrumenter,是 Istanbul 的核心,它的做用是「裝飾」源代碼,注入計數器。要往源代碼中注入計數器就須要識別代碼行、語句和函數等。首先讀取指定目錄(用戶配置)下的源碼並一一構造語法樹(AST),區分出四個維度的代碼段並進行標記,這個功能的具體實現邏輯本文不做詳細展開,有興趣的能夠去看下源碼或者 babel/parser 這個插件。簡單來講裝飾器的工做流就是:npm

流程圖

仍是以爲抽象?來看一個直觀的例子就明白了,好比待測試的源碼文件爲:前端工程化

function AFunctionThatNeverBeCalled () {
    return Math.random() > 0.5 ? true : false
}
function AFunctionThatWillBeCalled (string) {
    return string
}
module.exports = function sayHello (name) {
    if (name) {
        return AFunctionThatWillBeCalled('Hello, ' + name)
    } else {
        return 'Should pass a name'
    }
}
複製代碼

通過裝飾器的 AST、維度標記等操做處理後,源碼就被裝飾成了這個樣子:bash

var cov_1pwyfn0t92 = (function() {
  // 此處省略較多的代碼,這裏面返回的是一個計數器對象,包括 AST 解析數據等,詳見下文
})();
function AFunctionThatNeverBeCalled() {
  cov_1pwyfn0t92.f[0]++;
  cov_1pwyfn0t92.s[0]++;
  return Math.random() > 0.2
    ? (cov_1pwyfn0t92.b[0][0]++, true)
    : (cov_1pwyfn0t92.b[0][1]++, false);
}
function AFunctionThatWillBeCalled(string) {
  cov_1pwyfn0t92.f[1]++;
  cov_1pwyfn0t92.s[1]++;
  return string;
}
cov_1pwyfn0t92.s[2]++;
module.exports = function sayHello(name) {
  cov_1pwyfn0t92.f[2]++;
  cov_1pwyfn0t92.s[3]++;
  if (name) {
    cov_1pwyfn0t92.b[1][0]++;
    cov_1pwyfn0t92.s[4]++;
    return AFunctionThatWillBeCalled('Hello, ' + name);
  } else {
    cov_1pwyfn0t92.b[1][1]++;
    cov_1pwyfn0t92.s[5]++;
    return 'Should pass a name';
  }
};
複製代碼

能夠看到最開始的源代碼幾乎被轉換成了另外一個樣子,但原來的代碼邏輯是不會改變的,只是注入了一些對原代碼執行沒有影響的計數語句,很明顯這些計數代碼就對應了各個維度的計數器:babel

| ------ | ------ | | cov_1pwyfn0t92 | 文件惟一計數對象| | cov_1pwyfn0t92.s | Statement 計數器 | | cov_1pwyfn0t92.b | Branch 計數器 | | cov_1pwyfn0t92.f | Function 計數器 |前端工程師

細心的朋友可能發現缺乏了行覆蓋率指標 Lines 計數器,其實行覆蓋率是經過語句中的起始行和結束行之間語句的執行率計算得來的。若是再把 cov_1pwyfn0t92 這個對象展開來看裏面的內容,那麼通過裝飾器「裝飾」後的產出和解析結果就更加直觀明瞭了:

一句話總結裝飾器的做用就是:篡改源代碼,注入計數器

第二步:攔截模塊加載器

可是實際上總不能真的把源碼給改了吧,那麼 Istanbul 是如何讓測試用例引用的源代碼變成本身篡改過的代碼呢?當單元測試框架(jest、mocha 等)開始跑(執行)測試用例的時候,只要把當前運行時的模塊加載器要加載的源代碼攔截掉,換成 Istanbul 裝飾過的代碼便可,也就是對測試用例所引用到的源代碼進行「偷樑換柱」:

攔截圖解

Istanbul 實現 addRequireHook 方法是用了一個 npm 模塊 append-transform,大體原理就是相似 nodejs 的 require.extensions 和增長一些特殊處理,具體的細節就不詳述了,這裏只須要知道它起到了攔截加載器的做用。

第三步:統計和輸出覆蓋率報告

通過了前面的步驟,已經篡改了源代碼並注入計數器,那麼執行完測試用例後再去收集每個文件的四個指標覆蓋率就水到渠成了,最後拿到結果就能夠輸出直觀的統計報告,Istanbul 支持輸出多種統計報告類型:

統計報告類型

每一種類型都有對應的獨立模塊去處理,好比 html 類型的報告須要生成直觀的 html 文件;lcov 則須要生成二進制文件等等。

這就是 Istanbul 的基本實現原理,本文只是描述了最主幹的實現,一些細節的功能好比忽略代碼塊、ES6 的支持和 SourceMap 等等也很是值得去閱讀和深挖。

參考: github.com/istanbuljs/…


AlloyTeam 歡迎優秀的小夥伴加入。
簡歷投遞: alloyteam@qq.com
詳情可點擊 騰訊AlloyTeam招募Web前端工程師(社招)

相關文章
相關標籤/搜索