CLASS 02 · 2026-02-04 · 基础

测试

测试是验证软件正确性的核心手段,但穷举测试不可行。本讲介绍如何通过「先写规格、再写测试、最后实现」的流程,系统化地划分输入空间、选取边界值,设计出既正确又精简的测试套件。

为什么软件测试很难

测试是更一般的验证(validation)过程的一个实例。理论上我们想验证「程序对所有合法输入都给出正确结果」,但输入空间往往大到无法穷举——穷举测试是不可行的,因此测试用例必须经过精心而系统的挑选。

测试优先编程

正确的开发顺序不是「先实现再补测试」,而是反过来:

  1. 规格(Spec):先写出函数的规格说明(它应该做什么)。
  2. 测试(Test):根据规格编写能检验该规格的测试用例。
  3. 实现(Implement):最后才写让测试通过的代码。

这样做的好处是:测试用例源自规格而非实现细节,因此即便将来重写实现,测试依然有效。

好测试套件的三条标准

这三者之间存在张力:充分往往意味着更多用例,而精简要求压缩用例。系统化的划分方法是平衡它们的关键。

通过划分选取测试用例

把程序的输入空间划分为若干子域(subdomains),每个子域是一组具有相似行为的输入,然后从每个子域中至少选取一个代表作为测试用例。

以求绝对值函数为例,输入空间可以划分为两个子域:\(x \ge 0\) 和 \(x < 0\)。

/**
 * 返回一个数的绝对值。
 * @param x 任意实数
 * @returns x 的绝对值
 */
function abs(x: number): number {
  if (x < 0) return -x;
  else return x;
}

// 测试用例:从每个子域各取一个代表
// 子域 x >= 0 → 取 5,期望返回 5
// 子域 x < 0  → 取 -3,期望返回 3

划分必须包含边界

bug 经常出现在子域的边界上。因此划分不仅要覆盖每个子域内部,还必须显式地包含边界点。对于 abs,边界点 \(x = 0\) 必须单独测试:

// 边界用例:x = 0,期望返回 0
// 这条用例最容易暴露 "x <= 0" 和 "x < 0" 的混淆

黑盒测试与白盒测试

白盒测试仍需遵循规格——它的作用是补充规格可能遗漏的路径覆盖,而不是替代规格。

代码覆盖率

衡量测试充分程度的常用指标:

分支覆盖强于语句覆盖,路径覆盖最强但往往不可行(循环会引入无穷多条路径)。实践中通常以分支覆盖为最低要求。

单元测试与集成测试

JavaScript/TypeScript 生态中,Mocha 是流行的单元测试框架。

自动化回归测试

每次修改代码后自动运行全部测试,称为回归测试(regression testing)。它的目的是确保新改动没有破坏原有功能——这正是「免于错误」和「便于修改」的保障。

迭代式测试优先编程

实际开发中,「规格 → 测试 → 实现」是一个迭代循环。先实现和测试一部分功能,获取反馈,再逐步扩展。不要试图一次写出完整的规格和全部测试。

随机化测试

除了手工选取用例,还可以借助自动化手段:

核心要点

测试优先先写规格,再写测试,最后写实现;测试用例应源自规格。

系统化划分把输入空间划分为子域,从每个子域取代表,并显式包含边界。

回归测试每次改动后自动运行全部测试,防止破坏已有功能。