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

了解什么是代码覆盖率,以及衡量覆盖率的四种常用方法。

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

什么是代码覆盖率?

代码覆盖率是一项指标,用于衡量测试所执行的源代码所占的百分比。它可以帮助您确定可能缺少适当测试的方面。

通常,记录这些指标的过程如下:

文件 对账单占比 分支所占百分比 % 函数 百分比线 未覆盖的线条
file.js 90% 100% 90% 80% 89256
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);
}

“代码行覆盖率”衡量您的测试套件已执行的可执行代码行所占的百分比。如果某行代码仍未执行,则表示代码的某些部分尚未测试。

此代码示例包含 8 行可执行代码(以红色和绿色突出显示),但测试不会执行 americano 条件(2 行)和 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. 仅使用 coffeeNameChek 标志。拨打 calcCoffeeIngredient
  2. 正在与coffeeNamecup联系calcCoffeeIngredientChek 标志。
  3. 浓缩咖啡 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;

测试仅涵盖九个语句中的五个,因此语句覆盖率为 55.55%。

如果您总是在每行只写一条语句,则行覆盖率将与语句覆盖率大致相当。

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

大多数代码覆盖率工具都包含这四种常见的代码覆盖率。选择要优先使用哪个代码覆盖率指标取决于具体的项目要求、开发做法和测试目标。

一般来说,语句覆盖率是一个很好的指标,因为它是一个简单易懂的指标。与语句覆盖率和函数覆盖率不同,分支覆盖率和函数覆盖率衡量测试是调用条件(分支)还是函数。因此,它们是语句覆盖后的自然进程。

达到较高的语句覆盖率后,您就可以继续进行分支覆盖率和函数覆盖率。

测试覆盖率与代码覆盖率是否相同?

不会。测试覆盖率和代码覆盖率经常相混淆,但两者并不相同:

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

用一个简单的类比:将 Web 应用想象成房子。

  • 测试覆盖范围用于衡量测试对住宅内房间的覆盖程度。
  • 代码覆盖率衡量测试走过了多少房屋。

100% 代码覆盖率并不意味着没有任何 bug

虽然测试时实现较高的代码覆盖率固然不错,但 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({});
  });
});