瞭解程式碼涵蓋範圍,並探索評估程式碼的四種常見方式。
您是否聽過「程式碼涵蓋率」這個詞?在這篇文章中,我們將探索測試中的程式碼涵蓋率,以及評估程式碼的四種常見方式。
什麼是程式碼涵蓋率?
「程式碼涵蓋率」是一項指標,可衡量測試執行的原始碼百分比。協助你找出未經過適當檢測的區域。
記錄這些指標通常如下所示:
檔案 | % 陳述 | % 分支版本 | % 函式 | % 行 | 隱藏的線條 |
---|---|---|---|---|---|
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 {};
}
…
分支版本涵蓋率可評估程式碼中執行的分支版本或決策點百分比,例如陳述式或迴圈。用於判定測試是否同時檢查條件陳述式的真實和錯誤分支。
程式碼範例包含五個分支版本:
- 正在撥打電話給
calcCoffeeIngredient
,只要coffeeName
- 正在與
coffeeName
和cup
() 撥打電話給calcCoffeeIngredient
- 咖啡是「Espresso」
- 咖啡是 Americano
- 其他咖啡
測試範圍涵蓋 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;
這項測試只涵蓋 9 個陳述式,因此陳述式涵蓋率為 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 測試的程式碼涵蓋率並不容易且困難。與其使用程式碼涵蓋率,或許是更好的做法。這是因為 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({});
});
});