四种常见的代码覆盖率测试

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

什么是代码覆盖率

代码覆盖率是衡量测试代码测试了源代码百分比多少的指标。它可以帮助您识别可能缺乏适当测试的代码区域。

通常,覆盖率指标会这样去记录:

File % Statements % Branch % Functions % Lines Uncovered lines
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);
}

不是很懂英语,去查了一下分别是:espresso-浓缩咖啡,americano-美式咖啡,mocha-摩卡咖啡

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

您可以在此demo中运行代码和测试,也可以签出存储库。

函数覆盖率

代码覆盖率:50%

/* coffee.js */
 
export function calcCoffeeIngredient(coffeeName, cup = 1) {
  // ...
}
 
function isValidCoffee(name) {
  // ...
}

功能覆盖率是一个简单的指标。它表示计算出测试代码调用了源代码中百分之多少函数。

在代码示例中,有两个函数:calcCoffeeIngredient和isValidCoffee。测试代码只调用calcCoffeeIngredient函数,因此函数覆盖率为50%。

行覆盖率

代码覆盖率:62.5%

/* coffee.js */
 
export function calcCoffeeIngredient(coffeeName, cup = 1) {
  let espresso, water;
 
  if (coffeeName === 'espresso') { // 1
    espresso = 30 * cup;  // 2
    return { espresso };  // 3
  }
 
  if (coffeeName === 'americano') {  // 4
    espresso = 30 * cup; water = 70 * cup; // 5
    return { espresso, water };  // 6
  }
 
  return {};  // 7
}
 
export function isValidCoffee(name) {
  return ['espresso', 'americano', 'mocha'].includes(name);  // 8
}

行覆盖率表示测试代码覆盖源代码的可执行代码行的百分比。如果一行代码仍然未执行,这意味着代码的某些部分没有经过测试。

代码示例有8行可执行代码,但是测试不执行americano条件(两行)和isValidCoffee函数(一行)。这使得线路覆盖率达到62.5%。

分支覆盖率

代码覆盖率:80%

/* coffee.js */
 
export function calcCoffeeIngredient(coffeeName, cup = 1) {
  // ...
 
  if (coffeeName === 'espresso') {
    // ...
    return { espresso };
  }
 
  if (coffeeName === 'americano') {
    // ...
    return { espresso, water };
  }
 
  return {};
}
…

分支覆盖率表示代码中执行分支或决策点的百分比,例如if语句或循环。它测定测试是否检查条件语句为true和false的分支。

代码示例中有五个分支:

只使用coffeeName调用calccoffeingredient √
用coffeeName和cup调用calcCoffeeIngredient √
coffeeName 是 浓缩咖啡 √
coffeeName 是 美式 ×
其他咖啡 √

测试涵盖除了咖啡是美式咖啡条件所有分支,所以分支覆盖率是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);
}

语句覆盖率检测测试代码执行了代码中百分之几的语句。乍一看,您可能会想,这与线路覆盖不一样吗?实际上,语句覆盖类似于行覆盖,但考虑的是包含多个语句的单行代码。

在代码示例中,有8行可执行代码,但是有9条语句。你能找出包含两个语句的行吗?

答案揭晓:一行有两个语句的代码:espresso = 30 * cup; water = 70 * cup;

测试只覆盖了9条语句中的5条,因此语句覆盖率为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%的代码覆盖率不是目标。相反,您应该使用代码覆盖以及一个包含各种测试方法的全面测试计划,包括单元测试、集成测试、端到端测试。

请参阅完整的代码示例,并使用良好的代码覆盖率进行测试。您还可以使用这个demo运行代码和测试。

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

最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!  

你可能感兴趣的:(软件测试,代码覆盖率,软件测试,功能测试,自动化测试,程序人生,职场和发展)