瞭解程式碼涵蓋率的定義,並探索四種常見的評估方式。
您是否聽過「程式碼涵蓋率」這個詞?本文將說明測試中的程式碼涵蓋率,以及四種常見的評估方式。
什麼是程式碼涵蓋率?
程式碼涵蓋率是用來評估測試執行的原始碼百分比的指標。這有助於您找出可能未經過適當測試的部分。
通常,記錄這些指標的情況如下所示:
檔案 | % 語句 | % Branch | % 功能 | % 行 | 未覆蓋的線條 |
---|---|---|---|---|---|
file.js | 90% | 100% | 90% | 80% | 89,256 |
coffee.js | 55.55% | 80% | 50% | 62.5% | 10-11, 18 |
新增功能和測試時,提高程式碼涵蓋率百分比,可讓您更有信心確保應用程式已經過徹底測試。不過,還有更多內容等你來探索。
四種常見的程式碼涵蓋率
收集及計算程式碼涵蓋率的方法有四種:函式、行、分支和陳述式涵蓋率。
如要瞭解各類型程式碼涵蓋率的計算方式,請參考以下用於計算咖啡成分的程式碼範例:
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
let espresso, water;
if (coffeeName === 'espresso') {
espresso = 30 * cup;
return { espresso };
}
if (coffeeName === 'americano') {
espresso = 30 * cup; water = 70 * cup;
return { espresso, water };
}
return {};
}
export function isValidCoffee(name) {
return ['espresso', 'americano', 'mocha'].includes(name);
}
驗證 calcCoffeeIngredient
函式的測試如下:
/* coffee.test.js */
import { describe, expect, assert, it } from 'vitest';
import { calcCoffeeIngredient } from '../src/coffee-incomplete';
describe('Coffee', () => {
it('should have espresso', () => {
const result = calcCoffeeIngredient('espresso', 2);
expect(result).to.deep.equal({ espresso: 60 });
});
it('should have nothing', () => {
const result = calcCoffeeIngredient('unknown');
expect(result).to.deep.equal({});
});
});
函式涵蓋率
程式碼涵蓋率:50%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
// ...
}
function isValidCoffee(name) {
// ...
}
函式涵蓋率是簡單的指標。這項指標會擷取測試呼叫的程式碼函式百分比。
在程式碼範例中,有兩個函式:calcCoffeeIngredient
和 isValidCoffee
。測試只會呼叫 calcCoffeeIngredient
函式,因此函式涵蓋率為 50%。
行涵蓋率
程式碼涵蓋率:62.5%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
let espresso, water;
if (coffeeName === 'espresso') {
espresso = 30 * cup;
return { espresso };
}
if (coffeeName === 'americano') {
espresso = 30 * cup; water = 70 * cup;
return { espresso, water };
}
return {};
}
export function isValidCoffee(name) {
return ['espresso', 'americano', 'mocha'].includes(name);
}
行涵蓋率會評估測試套件執行的可執行程式碼行百分比。如果程式碼行未執行,表示程式碼的某些部分尚未經過測試。
程式碼範例包含八行可執行的程式碼 (以紅色和綠色標示),但測試不會執行 americano
條件 (兩行) 和 isValidCoffee
函式 (一行)。這會導致行涵蓋率為 62.5%。
請注意,行程覆蓋率不會考量宣告陳述式 (例如 function isValidCoffee(name)
和 let espresso, water;
),因為這些陳述式無法執行。
分支版本涵蓋率
程式碼涵蓋率:80%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
// ...
if (coffeeName === 'espresso') {
// ...
return { espresso };
}
if (coffeeName === 'americano') {
// ...
return { espresso, water };
}
return {};
}
…
分支涵蓋率會評估程式碼中執行的分支或決策點的百分比,例如 if 陳述式或迴圈。它會判斷測試是否會檢查條件陳述式的 true 和 false 分支。
程式碼範例中有五個分支:
- 只使用
coffeeName
呼叫calcCoffeeIngredient
- 使用
coffeeName
和cup
呼叫calcCoffeeIngredient
- 咖啡是 Espresso
- 咖啡是美式咖啡
- 其他咖啡
測試涵蓋 Coffee is Americano
條件以外的所有分支。因此分支涵蓋率為 80%。
陳述式涵蓋範圍
程式碼涵蓋率:55.55%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
let espresso, water;
if (coffeeName === 'espresso') {
espresso = 30 * cup;
return { espresso };
}
if (coffeeName === 'americano') {
espresso = 30 * cup; water = 70 * cup;
return { espresso, water };
}
return {};
}
export function isValidCoffee(name) {
return ['espresso', 'americano', 'mocha'].includes(name);
}
陳述式涵蓋率會評估測試執行的程式碼中陳述式百分比。乍看之下,您可能會想:「這不是和行涵蓋率相同嗎?」事實上,陳述式涵蓋率與行涵蓋率相似,但會考量包含多個陳述式的單行程式碼。
在程式碼範例中,有八行可執行的程式碼,但有九個陳述式。您能找出包含兩個陳述式的行嗎?
espresso = 30 * cup; water = 70 * cup;
測試只涵蓋九個陳述式中的五個,因此陳述式涵蓋率為 55.55%。
如果您一律在每行中寫入一個陳述式,則行覆蓋率會與陳述式覆蓋率相似。
您應該選擇哪種類型的程式碼涵蓋率?
大部分的程式碼涵蓋率工具都包含這四種常見的程式碼涵蓋率。選擇要優先處理哪些程式碼涵蓋率指標,取決於特定專案需求、開發作業方式和測試目標。
一般來說,陳述式涵蓋率是個不錯的起點,因為它是簡單易懂的指標。與陳述式涵蓋率不同,分支涵蓋率和函式涵蓋率會評估測試是否呼叫條件 (分支) 或函式。因此,在完成語句涵蓋率後,自然會開始進行這類測試。
達到高語句涵蓋率後,您就可以繼續處理分支涵蓋率和函式涵蓋率。
測試涵蓋率是否與程式碼涵蓋率相同?
不,測試涵蓋率和程式碼涵蓋率經常會讓人混淆,但兩者並不相同:
- 測試涵蓋率:評估測試套件涵蓋軟體功能的程度的定性指標。這有助於判斷相關風險等級。
- 程式碼涵蓋率:用於評估測試期間執行的程式碼比例。而是測試涵蓋的程式碼量。
以下是簡化的比喻:請將網頁應用程式視為一棟房子。
- 測試涵蓋率可評估測試涵蓋房屋內各個房間的程度。
- 程式碼涵蓋率可評估測試涵蓋的房屋比例。
程式碼涵蓋率達到 100% 不代表沒有錯誤
雖然在測試中達到高程式碼涵蓋率確實是理想情況,但 100% 的程式碼涵蓋率並不能保證程式碼中沒有錯誤或瑕疵。
達成 100% 程式碼涵蓋率的無意義方式
請考慮以下測試:
/* coffee.test.js */
// ...
describe('Warning: Do not do this', () => {
it('is meaningless', () => {
calcCoffeeIngredient('espresso', 2);
calcCoffeeIngredient('americano');
calcCoffeeIngredient('unknown');
isValidCoffee('mocha');
expect(true).toBe(true); // not meaningful assertion
});
});
這項測試可達到 100% 的函式、行、分支和陳述式涵蓋率,但這並不合理,因為它並未實際測試程式碼。無論程式碼是否正常運作,expect(true).toBe(true)
斷言一律會通過。
使用不良指標比不使用指標更糟糕
不良指標可能會讓您產生安全無虞的錯覺,這比完全沒有指標還糟。舉例來說,如果您的測試套件達到 100% 程式碼涵蓋率,但測試都沒有意義,您可能會誤以為程式碼已經過良好測試,如果您不小心刪除或破壞部分應用程式程式碼,即使應用程式無法正常運作,測試仍會通過。
如要避免這種情況,請採取下列做法:
- 測試審查。編寫及審查測試,確保測試有意義,並在各種不同情境下測試程式碼。
- 將程式碼涵蓋率做為指南,而非測試成效或程式碼品質的唯一評估標準。
在不同類型的測試中使用程式碼涵蓋率
讓我們進一步瞭解如何使用程式碼涵蓋率搭配三種常見的測試類型:
- 單元測試。這類測試是收集程式碼涵蓋率的最佳測試類型,因為它們旨在涵蓋多個小型情境和測試路徑。
- 整合測試。這些測試可協助收集整合測試的程式碼涵蓋率,但請謹慎使用。在這種情況下,您會計算較大範圍的原始碼涵蓋率,因此很難判斷哪些測試實際涵蓋程式碼的哪些部分。不過,如果舊版系統沒有妥善隔離的單元,計算整合測試的程式碼涵蓋率可能會很有幫助。
- 端對端 (E2E) 測試。由於端對端測試的複雜性,因此很難評估程式碼涵蓋率。與其使用程式碼涵蓋率,不如採用需求涵蓋率,因為 E2E 測試的重點是涵蓋測試需求,而非著重於原始碼。
結論
程式碼涵蓋率是評估測試成效的實用指標。這有助於確保程式碼中的重要邏輯經過充分測試,進而提升應用程式品質。
不過,請記住,程式碼涵蓋率只是其中一個指標。請務必考量其他因素,例如測試品質和應用程式需求。
我們並未以 100% 的程式碼涵蓋率為目標。相反地,您應使用程式碼涵蓋率,並搭配完善的測試計畫,其中包含各種測試方法,包括單元測試、整合測試、端對端測試和手動測試。
查看完整程式碼範例和測試,以便涵蓋大部分程式碼。您也可以透過這個即時示範執行程式碼和測試。
/* coffee.js - a complete example */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
if (!isValidCoffee(coffeeName)) return {};
let espresso, water;
if (coffeeName === 'espresso') {
espresso = 30 * cup;
return { espresso };
}
if (coffeeName === 'americano') {
espresso = 30 * cup; water = 70 * cup;
return { espresso, water };
}
throw new Error (`${coffeeName} not found`);
}
function isValidCoffee(name) {
return ['espresso', 'americano', 'mocha'].includes(name);
}
/* coffee.test.js - a complete test suite */
import { describe, expect, it } from 'vitest';
import { calcCoffeeIngredient } from '../src/coffee-complete';
describe('Coffee', () => {
it('should have espresso', () => {
const result = calcCoffeeIngredient('espresso', 2);
expect(result).to.deep.equal({ espresso: 60 });
});
it('should have americano', () => {
const result = calcCoffeeIngredient('americano');
expect(result.espresso).to.equal(30);
expect(result.water).to.equal(70);
});
it('should throw error', () => {
const func = () => calcCoffeeIngredient('mocha');
expect(func).toThrowError(new Error('mocha not found'));
});
it('should have nothing', () => {
const result = calcCoffeeIngredient('unknown')
expect(result).to.deep.equal({});
});
});