不要在单个 `it` 里堆砌多个 `assert`:Mocha 测试拆分的正确姿势(Modern JavaScript Tutorial 实战解析)

原创2026-09-30 18:44:161,366 阅读
文章标签:文档/教程前端

不要在单个 it 里堆砌多个 assert:Mocha 测试拆分的正确姿势(Modern JavaScript Tutorial 实战解析)

本篇文章以 en.javascript.info 仓库中 "What's wrong in the test?" 这道习题及其参考答案为核心,剖析 BDD 测试中的一个典型反模式——"一个 it 块里塞三个 assert",并给出两种最佳实践:把测试拆分为多个独立的 it 块、以及用 it.only 隔离单个用例调试。读完本文,你将能写出失败信息清晰、可单独运行、易于定位问题的 Mocha 测试。

问题测试:语法正确、运行通过,但暗藏陷阱

题目给出的 pow 测试如下(源码见 task.md):

it("Raises x to the power n", function() {
  let x = 5;

  let result = x;
  assert.equal(pow(x, 1), result);

  result *= x;
  assert.equal(pow(x, 2), result);

  result *= x;
  assert.equal(pow(x, 3), result);
});

表面上看,这段代码没有任何语法问题,三个 assert.equal 也都能通过。它"看起来"测了三个用例:pow(5,1)、pow(5,2)、pow(5,3)。但正如参考答案开篇指出的——它实际上是 3 个测试,却被塞进了 1 个函数、用 3 个 assert 拼凑而成:

"The test demonstrates one of the temptations a developer meets when writing tests. What we have here is actually 3 tests, but layed out as a single function with 3 asserts."

这种写法之所以诱人,是因为"有时这样写更省事"("Sometimes it's easier to write this way")。然而一旦某个断言失败,问题就暴露了。

为什么是反模式:失败时你不得不"调试测试本身"

参考答案给出了核心论点:错误发生时,这个 it 块立刻终止,后面所有断言都不会执行,你能获取的诊断信息极其有限。

设想一个更复杂的场景——执行流程中途出错:

  • 你只知道"某个用例失败了",却不知道是哪个参数、哪组输入、失败时的中间数据是什么;
  • 由于断言之间共享变量(如题目里的 result),你甚至要逆向推算走到第几个 assert 时数据变成了什么;
  • 结果是:你不得不去调试测试代码本身("We'll actually have to debug the test"),而不是调试被测函数。

这正是参考文档反复强调的要点:单个 it 内多个断言,会让"哪个用例坏了"变得模糊,诊断成本直线上升。反观一个断言对应一个用例的写法,"if an error occurs, it's much less obvious what went wrong" 的问题就消失了。

正确姿势一:拆成多个 it 块,一测一事

参考答案给出的改进版本,是用一个 describe 包裹一组 it 块,每个 it 拥有清晰描述语和独立断言:

describe("Raises x to power n", function() {
  it("5 in the power of 1 equals 5", function() {
    assert.equal(pow(5, 1), 5);
  });

  it("5 in the power of 2 equals 25", function() {
    assert.equal(pow(5, 2), 25);
  });

  it("5 in the power of 3 equals 125", function() {
    assert.equal(pow(5, 3), 125);
  });
});

改动只有一处:把单个 it 换成 describe 加一组 it("We replaced the single it with describe and a group of it blocks")。收益立竿见影:

  • 失败信息自带上下文:一旦 pow(5, 2) 出错,测试报告会直接显示 "5 in the power of 2 equals 25" 这条用例失败,输入输出一目了然;
  • 互不牵连:一个断言失败不会终止同组其他断言,你能一次性看到所有失败项;
  • 符合"一个测试只测一件事"(One test checks one thing)的 BDD 原则——这条原则在 05-testing-mocha/article.md 的 "Improving the spec" 一节中有详细论述:看到两个相互独立的检查出现在同一个 it 里,就应该拆成两个更简单的测试。

正确姿势二:it.only 隔离单个用例,独立调试

参考答案还给出了另一个实战技巧:用 it.only 代替 it,Mocha 就只会运行这一个测试块("Mocha will run only this block"):

describe("Raises x to power n", function() {
  it("5 in the power of 1 equals 5", function() {
    assert.equal(pow(5, 1), 5);
  });

  // Mocha will run only this block
  it.only("5 in the power of 2 equals 25", function() {
    assert.equal(pow(5, 2), 25);
  });

  it("5 in the power of 3 equals 125", function() {
    assert.equal(pow(5, 3), 125);
  });
});

执行这段测试时,Mocha 会跳过其他所有 it,只跑标记了 it.only 的那个用例。这在两种场景下尤其有用:

  1. 定位失败:大规模测试失败时,先用 it.only 圈定可疑用例,把注意力收敛到单个输入输出上;
  2. 迭代开发:BDD 工作流中"先写 spec、再写实现、反复让测试变绿"(见 article.md 的 "The development flow"),it.only 让你在修改实现时只关注当前用例,跑得更快、反馈更直接。

注意:it.only 是调试手段,提交代码前务必移除,否则其余用例会被静默跳过,等于整个测试套件失去回归保护。

仓库源码印证:拆分思想贯穿整套测试体系

本仓库围绕 pow 演示了完整的 BDD 演进流程,测试拆分的思想在其中一以贯之。对照各阶段的真实测试文件,可以更直观地理解"一测一事"的分量:

  • 最小 spec:pow-1.view/test.js 只有一个 it("raises to n-th power"),断言 pow(2, 3) === 8,用于验证测试框架本身能跑通;
  • 拆分为两个用例:pow-2.view/test.js 把 pow(2,3) 与 pow(3,4) 拆成两个独立 it,结果第二个用例失败、而第一个仍通过——这正是"拆开比塞一起"的信息优势:你能立刻看到失败的是 3 raised to power 4 is 81,而不是整组测试一起红;
  • 循环生成用例:pow-3.view/test.js 用 for 循环 + makeTest 辅助函数批量生成 it,每个 it 依然只含一个断言;
  • 嵌套 describe 分组:pow-4.view/test.js 把 makeTest 和 for 收进嵌套的 describe("raises x to power 3") 子分组,输出报告自带缩进层级;
  • 完整 spec:pow-full.view/test.js 在此基础上新增 assert.isNaN(pow(2, -1)) 与 assert.isNaN(pow(2, 1.5)) 两个独立用例,分别验证负指数与非整数指数的边界行为。

再看运行环境:index.html 展示了浏览器端的完整测试页结构——<head> 引入 Mocha 与 Chai 后执行 mocha.setup('bdd')、<div id="mocha"> 承载结果输出、末尾 mocha.run() 启动测试;pow-full.view/index.html 则是把完整 pow 实现(含 n < 0 与 Math.round(n) != n 的 NaN 防护)与测试脚本组装起来的最终可运行版本。

这些文件共同印证了一个结论:无论是最小 spec 还是完整回归套件,每一个 it 都只承载一个独立断言——这既是"测试即文档"(it 的标题用自然语言描述用例)的基础,也是失败报告可读性的前提。

延伸:何时该拆分、何时可合写

拆分不是教条,而是基于信息论的成本权衡。参考文档的表述可以帮助你建立判断标准:

  • 当两个断言描述的是同一件事的两个阶段(比如先初始化再验证),可以留在同一个 it 里;但题目这种"三个独立幂次用例"显然属于三个独立行为,必须拆分;
  • 当你想从失败中获取最大信息量时,拆分是唯一正解——assert 抛错会立即终止 it 块,合写的测试在第一个断言失败后就"沉默"了(见 article.md 对 assert 行为与"One test checks one thing"的说明);
  • 当某个 it 中出现了两个互不依赖的检查,即使现在能通过,也要拆——因为将来重构时,它们失败的原因往往不同,合在一起会让你无从下手。

小结

这道习题的结论可以浓缩为三条可直接落地的规范:

  1. 一个 it 只测一件事:独立用例拆成独立 it,标题用自然语言写明输入输出(如 "5 in the power of 2 equals 25");
  2. 失败信息优先:断言失败会中断当前 it,合写断言等于主动丢弃诊断信息,还会迫使你"调试测试本身";
  3. 善用 it.only 隔离:定位问题或迭代实现时用它单跑一个用例,完成后务必移除。

把这三条应用到实际项目中,配合 describe 分组、beforeEach/afterEach 等 Mocha 机制(完整介绍见 05-testing-mocha/article.md),你就能写出既快又稳、失败时一眼定位问题的自动化测试。

登录后查看全文
en.javascript.info