什么是测试

编写软件时,您可以通过测试来确认其是否正常工作。测试可以广义地定义为以特定方式运行软件以确保其行为符合预期的过程。

成功的测试可以让您确信,当您添加新代码、功能甚至升级依赖项时,您已编写的软件将继续以您期望的方式工作。测试还可以帮助保护您的软件免受不太可能出现的情况或意外输入的影响。

您可能想要测试的一些 Web 行为示例包括

  • 确保当按钮被点击时网站的功能正常运行。
  • 确认复杂函数产生正确的结果。
  • 完成需要用户登录的操作。
  • 检查当输入格式错误的数据时表单是否正确报告错误。
  • 确保当用户带宽极低或离线时,复杂的 Web 应用继续运行。

自动化测试与手动测试

您可以通过两种通用方式测试您的软件:自动化测试和手动测试。

手动测试涉及人类直接运行软件,例如在其浏览器中加载网站,并确认其行为是否符合预期。手动测试很容易创建或定义 - 例如,您的站点可以加载吗?您可以执行这些操作吗?- 但每次运行都会花费大量的人工时间。虽然人类非常有创造力,这可以实现一种称为探索性测试的测试类型,但我们仍然可能不擅长注意到故障或不一致,尤其是在多次执行相同的任务时。

自动化测试是任何允许将测试编纂成代码并通过计算机重复运行以确认您的软件的预期行为而无需人类执行任何重复步骤(例如设置或检查结果)的过程。重要的是,一旦配置了自动化测试,就可以频繁运行它。这仍然是一个非常广泛的定义,值得注意的是,自动化测试具有各种形状和形式。本课程的大部分内容都与作为一种实践的自动化测试有关。

手动测试确实有其用武之地,通常作为编写自动化测试的先导,但也适用于自动化测试变得过于不可靠、范围过广或难以编写的情况。

通过示例了解基本原理

对于我们这些编写 JavaScript 或相关语言的 Web 开发者来说,一个简洁的自动化测试可能只是一个像这样的脚本,您可以每天运行它,可能是通过 Node,或者通过在浏览器中加载它

import { fibonacci } from "../src/math.js";

if (fibonacci(0) !== 0) {
  throw new Error("Invalid 0th fibonacci result");
}
const fib13 = fibonacci(13);
if (fib13 !== 233) {
  throw new Error("Invalid 13th fibonacci result, was=${fib13} wanted=233");
}

这是一个简化的示例,提供了以下见解

  • 这是一个测试,因为它运行一些软件(斐波那契函数),并通过将其结果与预期值进行比较来确保其行为符合预期。如果行为不正确,它会引发错误,JavaScript 通过抛出 Error 来表达这一点。

  • 即使您可能在终端或浏览器中手动运行此脚本,这仍然是一个自动化测试,因为它可以重复运行,而无需您执行任何单独的步骤。下一页“测试在哪里运行”对此进行了更详细的解释。

  • 即使此测试未使用任何库(它是可以在任何地方运行的 JavaScript),它仍然是一个测试。有许多工具可以帮助您编写测试,包括本课程稍后将介绍的工具,但它们都仍然基于基本原则工作,即如果出现问题则引发错误。

实践中的测试库

大多数库或内置测试框架都提供了两个主要原语,使测试更易于编写:断言和一种定义独立测试的方法。这些将在下一节“断言和其他原语”中详细介绍。但是,在高层面上,重要的是要记住,您看到或编写的几乎所有测试最终都将使用这些类型的原语。

断言是一种组合检查结果并在出现问题时引发错误的方法。例如,您可以通过引入 assert 来使之前的测试更加简洁

import { fibonacci } from "../src/math.js";
import { assert } from "a-made-up-testing-library";

assert.equal(fibonacci(0), 0, "Invalid 0th fibonacci result");
assert.equal(fibonacci(13), 233, "Invalid 13th fibonacci result");

您可以通过定义独立的测试(可以选择分组到套件中)来进一步改进此测试。以下套件独立测试了斐波那契函数和 卡塔兰函数

import { fibonacci, catalan } from "../src/math.js";
import { assert, test, suite } from "a-made-up-testing-library";

suite("math tests", () => {
  test("fibonacci function", () => {
    assert.equal(fibonacci(0), 0, "Invalid 0th fibonacci result");
    assert.equal(fibonacci(13), 233, "Invalid 13th fibonacci result");
  });
  test("relationship between sequences", () => {
    const numberToCheck = 4;
    const fib = fibonacci(numberToCheck);
    const cat = catalan(numberToCheck);
    assert.isAbove(fib, cat);
  });
});

在软件测试的上下文中,测试作为名词指的是测试用例:单个、独立、可寻址的场景,例如上例中的“序列之间的关系”测试用例。

单独命名的测试对于以下任务很有用,其中包括

  • 确定测试如何随时间推移成功或失败。
  • 通过名称突出显示错误或场景,以便您可以更轻松地测试该场景是否已解决。
  • 独立于其他测试运行某些测试,例如通过 glob 过滤器。

思考测试用例的一种方法是使用单元测试的“三个 A”:安排 (Arrange)、执行 (Act) 和断言 (Assert)。每个测试用例的核心都将

  • 安排一些值或状态(这可能只是硬编码的输入数据)。
  • 执行一个操作,例如调用一个方法。
  • 断言输出值或更新的状态(使用 assert)。

测试的规模

上一节中的代码示例描述了一个单元测试,因为它们测试您软件的次要部分,通常侧重于单个文件,在本例中,仅侧重于单个函数的输出。当您考虑来自多个文件、组件甚至不同的互连系统(有时超出您的控制范围,例如网络服务或外部依赖项的行为)的代码时,测试的复杂性会增加。因此,测试类型通常根据其范围规模命名。

除了单元测试之外,其他测试类型的一些示例包括组件测试可视化测试集成测试。这些名称都没有严格的定义,并且它们可能根据您的代码库而具有不同的含义,因此请记住将它们用作指南,并提出适合您的定义。例如,在您的系统中,被测组件是什么?对于 React 开发者来说,这可能实际上映射到“React 组件”,但对于其他上下文中的开发者来说,它可能具有不同的含义。

单个测试的规模可以将其置于通常称为“测试金字塔”的概念中,这对于测试检查的内容以及其运行方式可能是一个很好的经验法则。

The testing pyramid,
    with end-to-end (E2E) tests at the top, integration tests in the middle, and
    unit tests at the bottom.
测试金字塔。

这个想法已经过迭代,现在已经普及了各种其他形状,例如测试菱形或测试冰淇淋筒。您的测试编写优先级可能对您的代码库是唯一的。但是,一个共同的特征是,像单元测试这样的更简单的测试往往运行速度更快、更易于编写(因此您将拥有更多),并且测试范围有限,而像端到端测试这样的复杂测试很难编写,但可以测试更广泛的范围。事实上,许多测试“形状”的顶层往往是手动测试,因为某些用户交互太复杂而无法编纂成自动化测试。

这些类型将在“自动化测试的类型”中展开介绍。

检查您的理解情况

大多数测试库和框架提供哪些原语?

使用云提供商的运行器服务。
一些基于浏览器的运行器提供了一种外包测试的方法,但它不是测试库的正常功能。
如果断言不满足条件,则会引发异常的断言。
虽然您可以抛出错误来使测试失败,但 assert() 及其变体往往会包含在内,因为它们使检查更容易编写。
一种将测试分类到测试金字塔中的方法。
实际上没有标准方法来执行此操作。您可以为测试名称添加前缀,或将它们放在不同的文件中,但大多数测试框架实际上并未内置分类。
通过函数定义独立测试的能力。
test() 方法几乎包含在所有测试运行器中。这很重要,因为测试代码不在文件的顶层运行,这使测试运行器可以将每个测试用例视为一个独立的单元。