原文:Four common types of code coverage

作者:Ramona Schwering, Jecelyn Yeen

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

什么是代码掩盖率

代码掩盖率是衡量测验代码测验了源代码百分比多少的方针。它能够帮助您辨认可能缺少恰当测验的代码区域。

一般,掩盖率方针会这样去记录:

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) {
  // ...
}

功用掩盖率是一个简略的方针。它表明核算出测验代码调用了源代码中百分之多少函数。

在代码示例中,有两个函数:calcCoffeeIngredientisValidCoffee。测验代码只调用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的分支。

代码示例中有五个分支:

  1. 只运用coffeeName调用calccoffeingredient
  2. coffeeNamecup调用calcCoffeeIngredient
  3. coffeeName 是 浓缩咖啡 √
  4. coffeeName 是 美式
  5. 其他咖啡 √

测验包括除了咖啡是美式咖啡条件一切分支,所以分支掩盖率是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({});
  });
});