四種常見的程式碼涵蓋率

瞭解程式碼涵蓋率的定義,並探索四種常見的評估方式。

您是否聽過「程式碼涵蓋率」這個詞?本文將說明測試中的程式碼涵蓋率,以及四種常見的評估方式。

什麼是程式碼涵蓋率?

程式碼涵蓋率是用來評估測試執行的原始碼百分比的指標。這有助於您找出可能未經過適當測試的部分。

通常,記錄這些指標的情況如下所示:

檔案 % 語句 % 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) {
  // ...
}

函式涵蓋率是簡單的指標。這項指標會擷取測試呼叫的程式碼函式百分比。

在程式碼範例中,有兩個函式:calcCoffeeIngredientisValidCoffee。測試只會呼叫 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 分支。

程式碼範例中有五個分支:

  1. 只使用 coffeeName 勾號。 呼叫 calcCoffeeIngredient
  2. 使用 coffeeNamecup 勾號。 呼叫 calcCoffeeIngredient
  3. 咖啡是 Espresso 勾號。
  4. 咖啡是美式咖啡 X 號。
  5. 其他咖啡 勾號。

測試涵蓋 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({});
  });
});