四种常见的代码覆盖率类型

了解什么是代码覆盖率,并探索四种常见的衡量方法。

您是否听过“代码覆盖率”这个词?在这篇文章中,我们将探讨测试中的代码覆盖率是什么,以及四种常见的衡量方法。

什么是代码覆盖率?

代码覆盖率是一个指标,用于衡量测试执行的源代码百分比。它可以帮助您识别可能缺乏适当测试的区域。

通常,记录这些指标看起来像这样

文件 % 语句 % 分支 % 函数 % 行 未覆盖的行
file.js 90% 100% 90% 80% 89,256
coffee.js 55.55% 80% 50% 62.5% 10-11, 18

随着您添加新功能和测试,提高代码覆盖率百分比可以使您更有信心您的应用程序已得到充分测试。但是,还有更多内容需要探索。

四种常见的代码覆盖率类型

有四种常见的方法来收集和计算代码覆盖率:函数覆盖率、行覆盖率、分支覆盖率和语句覆盖率。

Four types of text coverage.

要了解每种类型的代码覆盖率如何计算其百分比,请考虑以下用于计算咖啡配料的代码示例

/* 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. 咖啡是浓缩咖啡 对勾标记。
  4. 咖啡是美式咖啡 叉号标记。
  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%。

如果您始终每行编写一个语句,则您的行覆盖率将与您的语句覆盖率相似。

您应该选择哪种类型的代码覆盖率?

大多数代码覆盖率工具都包含这四种常见的代码覆盖率类型。选择优先考虑哪种代码覆盖率指标取决于特定的项目要求、开发实践和测试目标。

一般来说,语句覆盖率是一个好的起点,因为它是一个简单且易于理解的指标。与语句覆盖率不同,分支覆盖率和函数覆盖率衡量测试是否调用了条件(分支)或函数。因此,它们是语句覆盖率之后的自然进展。

一旦您实现了高语句覆盖率,您就可以继续进行分支覆盖率和函数覆盖率。

测试覆盖率与代码覆盖率相同吗?

否。测试覆盖率和代码覆盖率经常被混淆,但它们是不同的

  • 测试覆盖率:一种定性指标,用于衡量测试套件对软件功能的覆盖程度。它有助于确定所涉及的风险级别。
  • 代码覆盖率:一种定量指标,用于衡量测试期间执行的代码比例。它关于测试覆盖了多少代码。

这是一个简化的类比:将 Web 应用程序想象成一栋房子。

  • 测试覆盖率衡量测试对房屋中房间的覆盖程度。
  • 代码覆盖率衡量测试已走过房屋的多少。

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({});
  });
});