跳到正文

我所理解的 JavaScript 最佳实践(15):用独立的预期值捕获实现错误

我所理解的 JavaScript 最佳实践

  1. 尽量去掉只用一次的中间变量
  2. 遍历的起点决定按 ID 取数的顺序
  3. 不用 reduce
  4. 按遍历任务选择循环和数组方法
  5. 异步遍历按任务依赖选择并发或串行
  6. 卫语句让主流程保持平直
  7. 用函数给可复用的业务规则命名
  8. 让对象字段保持有序
  9. 正确写注释
  10. 一行代码只做一件事
  11. 给函数起合适的名字
  12. 用空值合并运算符更好地控制默认值生效逻辑
  13. 用队列串行处理偏好更新,避免修改互相覆盖
  14. 确认弹窗关闭时,让等待中的 Promise 得到结果
  15. 用独立的预期值捕获实现错误(当前)

最近打开的文件按时间从新到旧排列,页面最多展示五项。这个需求很容易测。给六项输入,检查返回前五项。但如果测试直接拿生产代码里的数量上限生成预期值,那条限制即使被改坏,测试也可能跟着改答案。

实现和预期共用上限,错误会一起传过去

下例把函数和断言放在同一个文件里,可以直接用 Node.js 运行。输入是已经排好序的文件名。

import assert from 'node:assert/strict';

const recentDocumentLimit = 5;

function getRecentDocuments(documents) {
  return documents.slice(0, recentDocumentLimit);
}

const documents = ['A.md', 'B.md', 'C.md', 'D.md', 'E.md', 'F.md'];
assert.deepEqual(getRecentDocuments(documents), documents.slice(0, recentDocumentLimit));

运行后断言会通过。把 recentDocumentLimit 改成 4,断言还是通过。函数和预期都在使用 4,比较结果只证明两边用了一样的截取方式。需求里规定的五项从头到尾都没有参与判断。

让预期从需求里来

用同一组输入,把断言换成明确的五项结果。这里同时约束数量和顺序。读到断言,能直接看出第六项 F.md 不该出现。

assert.deepEqual(getRecentDocuments(documents), ['A.md', 'B.md', 'C.md', 'D.md', 'E.md']);

我用同样的六项输入运行了两种断言。只把上限从 5 改成 4,结果如下。

实现中的上限 用同一个上限计算预期 预期写成需求里的五项
5 通过 通过
4 仍然通过 断言失败,缺少 E.md

固定预期的价值在于它有独立依据。这里的五项来自产品规定。对于编码函数,依据也可以是协议给出的向量。对于计算逻辑,可以先手算一个小样本。预期如果长到无法人工核对,就先缩小输入,保留能区分正确实现和错误实现的那一部分。

用一个确定错误的实现检验断言

修 bug 时,可以先拿会触发问题的输入跑旧实现,看到断言因预期中的差异失败。实现已经修好时,也可以在隔离副本里临时恢复那个错误。像这里的数量上限,从 5 改成 4 就足以验证测试能抓住少一项的问题。

失败的位置也要对得上。这里应当报出缺少 E.md,模块导入失败或测试没启动都证明不了这一点。自动生成这类错误变体,再检查测试能否发现,是变异测试的基本做法,见 Stryker 的变异测试说明。

边界用例同样按职责选。如果列表只有两项,应该原样返回这两项。空列表应该仍为空。输入已经按时间排好,排序逻辑由负责生成输入的那一层验证。对这个函数来说,六项输入的关键就是第六项该被截掉、第五项必须留下。

测试代码当然可以共享夹具、类型和准备数据的工具。预期值该有自己的来处。看到两边用了同一份业务常量或同一条公式时,先想一个实际可能写错的值,再看看这条断言能不能把它抓出来。