四種常見的程式碼涵蓋率

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

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

什麼是程式碼涵蓋率?

程式碼涵蓋率是一項指標,可用來評估測試執行原始碼的百分比。有助於找出哪些部分需要適當測試。

記錄這些指標通常會像這樣:

檔案 % 聲明 % 分支版本 % 函式 % 線條 未顯示的線條
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 {};
}
…

「分支版本涵蓋範圍」會測量程式碼中已執行分支版本或決策點 (例如陳述式或迴圈) 的百分比。用於決定測試是否檢查條件陳述式的真及分支。

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

  1. 正在與calcCoffeeIngredient通話 (僅與coffeeName Chek 標誌。 聯絡)
  2. 正在與coffeeNamecup (Chek 標誌。) 撥打calcCoffeeIngredient
  3. 咖啡是 Espresso Chek 標誌。
  4. 咖啡是 X 標記。
  5. 其他咖啡 Chek 標誌。

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