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

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

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

代码覆盖率是一项指标,用于衡量测试所执行的源代码所占的百分比。这有助于您找出可能未经过适当测试的方面。

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

文件 % 语句 % 分支 % 函数 占行数百分比 未覆盖的行
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);
}

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

此代码示例包含 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 语句或循环)所占的百分比。它用于确定测试是否同时检查条件语句的真分支和假分支。

该代码示例中有五个分支:

  1. 仅使用 coffeeName Chek 标志。 调用 calcCoffeeIngredient
  2. 使用 coffeeNamecup 调用 calcCoffeeIngredient 对勾标记。
  3. 浓缩咖啡 Chek 标志。
  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%。

如果您始终在一行中编写一条语句,行覆盖率将与语句覆盖率相似。

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

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

一般来说,语句覆盖率是一个不错的起点,因为它是一个简单易懂的指标。与语句覆盖率不同,分支覆盖率和函数覆盖率用于衡量测试是调用条件(分支)还是函数。因此,它们是语句级覆盖率之后的自然延伸。

实现较高的语句覆盖率后,您可以继续提高分支覆盖率和函数覆盖率。

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

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

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

下面是一个简化的类比:假设 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 测试的复杂性质,衡量代码覆盖率既困难又具有挑战性。与使用代码覆盖率相比,使用要求覆盖率可能更好。这是因为端到端测试的重点是涵盖测试的要求,而不是关注源代码。

总结

代码覆盖率是一种衡量测试效果的实用指标。它可以确保对代码中的关键逻辑进行充分测试,从而帮助您提高应用质量。

不过,请注意,代码覆盖率只是一个指标。请务必考虑其他因素,例如测试质量和应用要求。

目标不是实现 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({});
  });
});