四種常見的程式碼涵蓋率

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

您是否聽過「程式碼涵蓋率」這個詞?在這篇文章中,我們將探索測試中的程式碼涵蓋率,以及評估程式碼的四種常見方式。

什麼是程式碼涵蓋率?

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

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

檔案 % 語句 % 分支版本 % 功能 % 行 隱藏的線條
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({});
  });
});