背景
最近我們前端團隊在重構大量的 UI 元件,為了保證程式碼質量,我要求團隊中的成員必須編寫單元測試,並且測試覆蓋率達到 80% 以上。那麼問題來了,為什麼是 80% 的覆蓋率? 這是一個硬性的考核指標嗎?
這裡所說的測試覆蓋率,是指的是開發人員寫的單元測試的覆蓋率,不是測試人員的功能測試的覆蓋率。
哪些地方需要寫單元測試?
為什麼需要寫單元測試就不再闡述,我相信大家都知道,特別是在持續整合過程中的重要性。但是,從我的經歷來看,當前的軟體市場環境中,不管是用的瀑布模式,還是螺旋模式,還是敏捷模式,很多軟體沒有寫單元測試。
我也是一個程式設計師,每天需要寫一些的業務程式碼,對於寫單元測試來說,確實需要我很多時間和精力,因為它也需要設計用例和一些體力活。所以在我們的一些專案中也存在很多功能沒有單元測試,主要原因有以下幾個點:
- 業務邏輯更新太快,單元測試不可複用;
- 業務時間緊急,迭代週期時間短,沒有時間寫單元測試;
- UI 上很多測試,通過單元測試程式碼無法覆蓋。
在《軟體測試》一書中講測試的原則,第一條就是:“完全測試程式是不可能的”。所以對於以上部分需不需測試,取決於你軟體性質,時間和團隊。但是對於滿足以下幾點程式碼我建議需要編寫單元測試:
- 和安全相關的程式碼邏輯;
- 核心的功能模組,函式;
- 短期不會發生變化的 UI 元件;
- 提供外部呼叫的介面。
測試覆蓋率報告
如果完全通過測試覆蓋作為質量標準是存在問題的,我們在檢查一個測試覆蓋了的時候往往會通過一些工具去檢查,程式設計師是可以通過一些方式讓數字看上去漂亮,但是這沒有意義。我們應該把它作為一種發現未被測試覆蓋的程式碼的手段,同時也是一種學習的手段,為什麼這段程式碼沒有覆蓋到? 如果這個函式的引數發生了變化會怎麼樣? 這段程式碼邏輯怎麼這麼複雜?
通過分析未被測試覆蓋的程式碼,找到是設計問題,還功能理解有問題,還是說著就是一段廢程式碼,它可以幫助開發者能夠更好的理解背後的事情,可以檢查程式中的廢程式碼,然後在以後的設計中做很好的抽象,做可測試的程式碼。
各種開發語言都有對應的測試框架,可以生成測試報告,在本文中我以前端的 javascript 為示例, karma
+ istanbul
工具生成報告。
karma
是一個測試框架;istanbul
是 JavaScript 程式的程式碼覆蓋率工具。
怎麼生成測試報告這裡就不講,有很多教程,也可以檢視官方文件 istanbul。這裡我們先來看一下生成出來的測試報告。 以下是 rsuite src/utils
目錄下檔案的測試報告, 這是開啟的一個生成 html
格式的測試報告:
- Statements: 語句覆蓋率,執行到每個語句;
- Branches: 分支覆蓋率,執行到每個if程式碼塊;
- Functions: 函式覆蓋率,呼叫到程式中的每一個函式;
- Lines: 行覆蓋率, 執行到程式中的每一行。
每一個指標都列出了覆蓋的比例和數量情況,其中
Statements
與Lines
比例和數量是一致的,那它們有什麼不同呢?
在程式碼中往往存在一些書寫不規範的情況,比如一行多個語句,這個時候它們統計的覆蓋率就會有差異。 這裡又有一個值得思考的問題就是,程式碼覆蓋率工具是怎麼統計一行多個語句這種程式碼的? 後面講到統計原理的時候會講到。
另外,我們通過圖中可以看出 decorate.js
這個檔案相對來說測試覆蓋率較低,我們進入再具體分析一下,在那些地方沒有覆蓋到:
從圖中我們可以看到紅色部分和黃色, 都是在測試用例中沒有覆蓋到的地方:
getProps
函式,該函式式export
出去的一個函式,但是在測試用例中沒有覆蓋到;typeof size === 'object'
程式碼塊沒有覆蓋到;Component.propTypes={}
.. 這裡黃色部分,是一個預設值設定,說明這個預設值一直沒有被使用過;
在圖中左側,顯示行號的地方有一個 12x
、9x
、4x
,這個代表了該行語句被執行的次數, 通過這個清晰的報告,我們可以在程式碼中看出那些函式,那些程式碼塊沒有被執行,從而去分析原因,修正測試用例,完善程式碼邏輯,提高質量。
生成測試報表原理
我先來看一下 istanbul
生成的測試報告中有個 lcov.info
檔案, 這裡我只貼出關於 decorate.js
檔案這部分的內容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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 覆蓋率統計的核心思想,是在原始碼相應的位置注入設定的統計程式碼,當執行測試程式碼的時候,程式碼執行到注入的地方,就會執行對應的統計程式碼,生成覆蓋率統計報告。大概步驟如下:
- 第一步:生成語法樹,對原始碼進行語法分析,解析,然後生成語法樹。
1234567891011121314> 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' }生成出來的結構如下,這段程式碼來自
esprima
, A simple example on Node.js REPL: - 第二步:注入統計程式碼,在語法樹相應的位置注入統計程式碼,在程式執行到這個位置的時候對相應的全域性變數賦值,確保執行之後能夠根據全域性變數知道程式碼的執行流程。到這裡就解決了前面說的“一行如果有多個語句怎麼統計?”的問題。
- 第三步:再把注入統計程式碼的語法樹,生成對應的 javascript 程式碼。
以下是
escodegen
的一段示例程式碼12345678// A simple example: the programescodegen.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 程式碼是有一些開源工具的,所以如果有興趣的童鞋要自己實現一套程式碼覆蓋率的功能,只需要寫好注入的統計程式碼邏輯和執行環境的處理。
總結
對一個持續整合的專案來說,單元測試非常重要,同時最好具有較高的測試覆蓋率。再次強調測試覆蓋率是一種發現未被測試覆蓋的程式碼的手段,它不是一個考核質量的目標。