代碼測試覆蓋率分析

背景

最近咱們前端團隊在重構大量的 UI 組件,爲了保證代碼質量,我要求團隊中的成員必須編寫單元測試,而且測試覆蓋率達到 80% 以上。那麼問題來了,爲何是 80% 的覆蓋率? 這是一個硬性的考覈指標嗎?javascript

這裏所說的測試覆蓋率,是指的是開發人員寫的單元測試的覆蓋率,不是測試人員的功能測試的覆蓋率。html

哪些地方須要寫單元測試?

爲何須要寫單元測試就再也不闡述,我相信你們都知道,特別是在持續集成過程當中的重要性。可是,從個人經從來看,當前的軟件市場環境中,不論是用的瀑布模式,仍是螺旋模式,仍是敏捷模式,不少軟件沒有寫單元測試。前端

我也是一個程序員,天天須要寫一些的業務代碼,對於寫單元測試來講,確實須要我不少時間和精力,由於它也須要設計用例和一些體力活。因此在咱們的一些項目中也存在不少功能沒有單元測試,主要緣由有如下幾個點:java

  • 業務邏輯更新太快,單元測試不可複用;node

  • 業務時間緊急,迭代週期時間短,沒有時間寫單元測試;git

  • UI 上不少測試,經過單元測試代碼沒法覆蓋。程序員

在《軟件測試》一書中講測試的原則,第一條就是:「徹底測試程序是不可能的」。因此對於以上部分需不需測試,取決於你軟件性質,時間和團隊。可是對於知足如下幾點代碼我建議須要編寫單元測試:github

  • 和安全相關的代碼邏輯;瀏覽器

  • 核心的功能模塊,函數;安全

  • 短時間不會發生變化的 UI 組件;

  • 提供外部調用的接口。

<!-- more -->

測試覆蓋率報告

若是徹底經過測試覆蓋做爲質量標準是存在問題的,咱們在檢查一個測試覆蓋了的時候每每會經過一些工具去檢查,程序員是能夠經過一些方式讓數字看上去漂亮,可是這沒有意義。咱們應該把它做爲一種發現未被測試覆蓋的代碼的手段,同時也是一種學習的手段,爲何這段代碼沒有覆蓋到? 若是這個函數的參數發生了變化會怎麼樣? 這段代碼邏輯怎麼這麼複雜?

經過分析未被測試覆蓋的代碼,找到是設計問題,還功能理解有問題,仍是說着就是一段廢代碼,它能夠幫助開發者可以更好的理解背後的事情,能夠檢查程序中的廢代碼,而後在之後的設計中作很好的抽象,作可測試的代碼。

各類開發語言都有對應的測試框架,能夠生成測試報告,在本文中我之前端的 javascript 爲示例, karma + istanbul 工具生成報告。

  • karma 是一個測試框架;

  • istanbul 是 JavaScript 程序的代碼覆蓋率工具。

怎麼生成測試報告這裏就不講,有不少教程,也能夠查看官方文檔 istanbul。這裏咱們先來看一下生成出來的測試報告。 如下是 rsuite src/utils 目錄下文件的測試報告, 這是打開的一個生成 html 格式的測試報告:

{% asset_img 1.png RSUITE 測試覆蓋率 %}

從圖中咱們能夠看到它有四個指標:

  • Statements: 語句覆蓋率,執行到每一個語句;

  • Branches: 分支覆蓋率,執行到每一個if代碼塊;

  • Functions: 函數覆蓋率,調用到程式中的每個函數;

  • Lines: 行覆蓋率, 執行到程序中的每一行。

每個指標都列出了覆蓋的比例和數量狀況,其中 StatementsLines 比例和數量是一致的,那它們有什麼不一樣呢?
在代碼中每每存在一些書寫不規範的狀況,好比一行多個語句,這個時候它們統計的覆蓋率就會有差別。 這裏又有一個值得思考的問題就是,代碼覆蓋率工具是怎麼統計一行多個語句這種代碼的? 後面講到統計原理的時候會講到。

另外,咱們經過圖中能夠看出 decorate.js 這個文件相對來講測試覆蓋率較低,咱們進入再具體分析一下,在那些地方沒有覆蓋到:

{% asset_img 2.png decorate.js 測試覆蓋率 %}

從圖中咱們能夠看到紅色部分和黃色, 都是在測試用例中沒有覆蓋到的地方:

  • getProps 函數,該函數式 export 出去的一個函數,可是在測試用例中沒有覆蓋到;

  • typeof size === 'object' 代碼塊沒有覆蓋到;

  • Component.propTypes={}.. 這裏黃色部分,是一個默認值設置,說明這個默認值一直沒有被使用過;

在圖中左側,顯示行號的地方有一個 12x9x4x,這個表明了該行語句被執行的次數, 經過這個清晰的報告,咱們能夠在代碼中看出那些函數,那些代碼塊沒有被執行,從而去分析緣由,修正測試用例,完善代碼邏輯,提升質量。

生成測試報表原理

我先來看一下 istanbul 生成的測試報告中有個 lcov.info 文件, 這裏我只貼出關於 decorate.js 文件這部分的內容:

SF:/Users/simonguo/workspace/rsuite/src/utils/decorate.js
FN:25,getClassNames
FN:39,getProps
FN:41,(anonymous_2)
FN:50,decorate
FN:51,(anonymous_4)
FNF:5
FNH:3
FNDA:237,getClassNames
FNDA:0,getProps
FNDA:0,(anonymous_2)
FNDA:12,decorate
FNDA:12,(anonymous_4)
DA:4,1
DA:11,1
DA:18,1
DA:27,237
DA:28,237
DA:30,237
DA:32,237
DA:40,0
DA:41,0
DA:42,0
DA:44,0
DA:51,12
DA:52,12
DA:53,12
DA:54,12
DA:56,12
...

FN 表明函數,
25,39,41,50,51 這些行分佈對應源代碼中的函數開始的行號,
FNF:5 表明一共有5個函數
FNH:3 其實 3 個函數被測試所覆蓋,
FNDA:237,getClassNames 表明了 getClassNames 這個函數被執行了 237 次。
...

等等,在文件中詳細記載了行號,以及代碼的執行狀況,你們能夠再對照前面的那張「測試覆蓋率」圖片進行分析,能夠詳細的看出整個 lcov.info 文件中記錄內容。有了這樣一份記錄信息就可以生成出一份可視化的測試報告,也能夠上傳到 coveralls,展現給你們。 那麼這裏須要思考的問題是,這樣一份數據統計記錄是怎麼統計出來的呢?

若是但願有些代碼被忽略,不進入覆蓋統計,istanbul 提供註釋語法 ,查看Ignoring code for coverage purposes

javascript 覆蓋率統計的核心思想,是在源代碼相應的位置注入設定的統計代碼,當執行測試代碼的時候,代碼運行到注入的地方,就會執行對應的統計代碼,生成覆蓋率統計報告。大概步驟以下:

  • 第一步:生成語法樹,對源代碼進行語法分析,解析,而後生成語法樹。

生成出來的結構以下,這段代碼來自 esprima, A simple example on Node.js REPL:

> var esprima = require('esprima');
> var program = 'const answer = 42';

> esprima.tokenize(program);
[ { type: 'Keyword', value: 'const' },
  { type: 'Identifier', value: 'answer' },
  { type: 'Punctuator', value: '=' },
  { type: 'Numeric', value: '42' } ]

> esprima.parse(program);
{ type: 'Program',
  body:
   [ { type: 'VariableDeclaration',
       declarations: [Object],
       kind: 'const' } ],
  sourceType: 'script' }
  • 第二步:注入統計代碼,在語法樹相應的位置注入統計代碼,在程序執行到這個位置的時候對相應的全局變量賦值,確保執行以後可以根據全局變量知道代碼的執行流程。到這裏就解決了前面說的「一行若是有多個語句怎麼統計?」的問題。

  • 第三步:再把注入統計代碼的語法樹,生成對應的 javascript 代碼。

如下是 escodegen 的一段示例代碼

// A simple example: the program

escodegen.generate({
    type: 'BinaryExpression',
    operator: '+',
    left: { type: 'Literal', value: 40 },
    right: { type: 'Literal', value: 2 }
});

// produces the string '40 + 2'.
  • 第四步:將生成好的 javascript 代碼交給執行環境(nodejs或者瀏覽器)運行。

  • 第五步:執行單元測試,產生的統計信息,放到全局標量中。

  • 第六步:根據全局標量中的覆蓋率信息生成特定格式的報告,這樣咱們就看到了 lcov.info 文件和 .html 文件。

這個步驟是依據 istanbul 統計 javasript 的原理,其餘語言的一些統計工具沒有接觸過,可是基本的思想應該都是大同小異的。在 javasript 對語法分析,生產語法樹再還原 javasript 代碼是有一些開源工具的,因此若是有興趣的童鞋要本身實現一套代碼覆蓋率的功能,只須要寫好注入的統計代碼邏輯和運行環境的處理。

總結

對一個持續集成的項目來講,單元測試很是重要,同時最好具備較高的測試覆蓋率。再次強調測試覆蓋率是一種發現未被測試覆蓋的代碼的手段,它不是一個考覈質量的目標。

另外,咱們維護的開源項目 rsuite ,是一套 React 的 UI 組件庫,若是你對此感興趣,或者使用中遇到任何問題,能夠聯繫咱們 Discord: join chat

本文做者:郭小銘

相關文章
相關標籤/搜索