什么是测试

编写软件时,您可以通过测试确认它能否正常运行。一般来说,测试是指以特定方式运行软件以确保其行为符合预期的过程。

成功的测试可以使您确信,在添加新代码、功能,甚至升级依赖项时,您已经编写的软件将继续以您预期的方式运行。测试还有助于保护您的软件,使其免受不太可能发生的情况或意外输入的影响。

您可能希望测试的一些 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");
}

这是一个简化的示例,可提供以下数据分析:

  • 这是一个测试,因为它会运行一些软件(Fibonacci 函数),并根据预期值检查结果,确保其行为符合预期。如果行为不正确,则会导致错误,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");

您可以通过定义独立的测试(可以选择性地划分到各个套件)来进一步改进此测试。以下套件独立测试了斐波那契函数和 Catalan 函数

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);
  });
});

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

单独命名的测试可用于完成以下任务等:

  • 确定测试在一段时间内成功或失败的情况。
  • 按名称突出显示 bug 或场景,以便您更轻松地测试场景是否已解决。
  • 单独运行某些测试,例如通过 glob 过滤器。

若要设计测试用例,一种方式是使用单元测试的“三个 A”:排列、操作和断言。每个测试用例的核心都将:

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

测试规模

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

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

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

测试金字塔,顶部是端到端 (E2E) 测试,中间是集成测试,底部是单元测试。
测试金字塔。

这一想法经过迭代后,其他各种形状现在逐渐普及,例如测试钻石或测试冰锥。您的测试编写优先级可能因代码库而异。不过,一个常见的特性是,较简单的测试(如单元测试)往往运行更快、更易于编写(这样将有更多),且测试的范围有限,而像端到端测试这样的复杂测试很难编写,但可以测试更广泛的范围。事实上,许多测试“形状”的顶层往往是手动测试,因为某些用户互动过于复杂,无法编码为自动化测试。

这些类型将在自动化测试的类型中加以扩展。

检查您的掌握程度

大多数测试库和框架都提供哪些基元?

使用云提供程序的运行程序服务。
一些基于浏览器的运行程序提供了一种将测试外包的方法,但这不是测试库的常规功能。
不满足要求时导致异常的断言。
虽然您可以抛出错误,导致测试失败,但系统往往会包含 assert() 及其变体,因为它们更易于编写检查。
一种将测试分类到测试金字塔的方法。
这其实并没有标准方法。您可以将测试名称作为前缀,也可以将它们放在不同的文件中,但大多数测试框架都没有构建分类功能。
能够按函数定义独立测试。
几乎所有测试运行程序中都包含 test() 方法。测试代码非常重要,因为测试代码不会在文件顶层运行,这使得测试运行程序能够将每个测试用例视为一个独立的单元。