现代 JavaScript 教程:Mocha 单元测试中“一个 it 塞进多个断言”的坑与正确写法

原创2026-10-05 09:48:29593 阅读
文章标签:文档教程

现代 JavaScript 教程:Mocha 单元测试中“一个 it 塞进多个断言”的坑与正确写法

导读:本文以 zh.javascript.info 仓库中《测试代码中有什么错误?》练习题为切入点,剖析一个语法正确、能全部通过却“有问题”的 Mocha 测试用例——它把 3 个 assert 断言塞进了同一个 it 代码块。读完本文,你将掌握 BDD 风格下“一个测试只检查一件事”的拆分原则、describe/it/assert 的组织方式,以及用 it.only 隔离调试单个测试的实战技巧。

一、题目原文与“语法正确但有问题”的测试

仓库中的练习题位于 1-js/03-code-quality/05-testing-mocha/3-pow-test-wrong/task.md,难度标记为 importance 5。题目给出一段针对幂函数 pow(x, n) 的测试代码,并询问:这段测试代码有什么错误?

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

题目的关键提示是:从语法上来说这些测试代码是正确且通过的。也就是说,这不是一个“报错”的坏测试,而是一个“能通过但设计不良”的测试。这正是本练习想要训练的判断力:测试代码的质量不能只看“绿不绿”,更要看“出了问题之后好不好定位”。

从源码结构看,本仓库为该练习仅提供 task.md 与 solution.md 两个文件,没有配套的 .view 可运行示例;但同一章 05-testing-mocha 目录下的 pow-1.view、pow-2.view、pow-full.view 等沙箱页面完整展示了 Mocha + Chai 的浏览器端运行环境,可作为理解本练习的对照素材。

二、错误在哪里:一个 it 里塞了 3 个独立断言

2.1 表面看:三个断言、三段连续计算

上面的代码逻辑上是在连续验证:

  • pow(5, 1) === 5(result 初值为 x);
  • pow(5, 2) === 25(result *= x 后为 25);
  • pow(5, 3) === 125(再乘一次后为 125)。

三个 assert.equal 语法完全合法,如果 pow 实现正确,整段测试会全部通过——正如题目附注所说“语法上是正确且通过的”。

2.2 实质问题:一个 it 实际承担了三个测试用例

按 solution.md 的官方解答,这段代码“实际上有三条测试,但是用一个函数放置了 3 个断言语句”。这样写有时确实更快,但存在一个致命缺陷:

如果发生错误,那么到底什么出错了就很不明显。 如果错误发生在一个复杂的执行流的中间,那么我们就必须找出那个点的数据,必须去“调试测试”。

在 Mocha 中,it 代码块内一旦某个 assert 抛错,该 it 会立即终止,其后的断言不会执行,且报错信息只指向这一个 it 块。于是你会面临两种糟糕情况:

  1. 前一个断言失败 → 后两个断言永远看不到结果,你无法一次获知 pow(5, 2)、pow(5, 3) 是否也坏了;
  2. 中间某个断言失败 → 报错信息无法直接告诉你是哪个幂次出了问题,你需要顺着 result 的累乘过程手动推算当前数据是什么。

这正是 article.md 中强调的核心原则的反面教材:“一个测试检查一个东西”(A test checks one thing)。如果在一个测试里发现两个相互独立的检查,应当拆成两个更简单的检查;独立的测试有助于你准确获知代码中正在发生什么。

三、正确写法:拆成一组带明确输入输出的 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);
  });
});

重构之后:

  • 定位成本极低:一旦失败,Mocha 输出的测试标题直接告诉你“哪个输入、哪个期望值”出问题了,例如 “5 in the power of 2 equals 25” 失败,就明确是 pow(5, 2) !== 25;
  • 互不干扰:一个用例失败不会吞掉其他用例的结果,所有断言都能被执行并分别报告;
  • 兼具文档价值:按 BDD 的理念,规范(spec)同时是测试、文档和示例,it 的标题本身就是函数行为的可读描述。

四、进阶技巧:用 it.only 隔离单个测试

在拆分后的测试套件中,如果只想单独运行某一个用例进行调试,Mocha 提供了 it.only(以及 describe.only):被标记的代码块是“唯一”会被执行的测试,其余测试被跳过。

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

  // Mocha 将只运行这个代码块
  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);
  });
});

这也是 solution.md 给出的官方补充技巧。注意 it.only 只用于临时调试,提交前应恢复为 it,否则会掩盖其他测试的真实状态。

五、对照仓库源码:正确的测试应该如何组织

5.1 基础规范的三要素

article.md 指出一个规范由三个主要模块构成:

  • describe("title", function() { ... }):描述正在测试的功能,用于组织 it 代码块;
  • it("use case description", function() { ... }):用易于理解的语言描述特定用例,第二个参数是执行测试的函数;
  • assert.equal(value1, value2):在 it 块中执行、若实现正确则不应抛错的检查语句;assert.* 系列函数用于验证 pow 是否符合预期。

仓库中的首个规范 pow-1.view/test.js 就是标准的三要素示范:

describe("pow", function() {

  it("raises to n-th power", function() {
    assert.equal(pow(2, 3), 8);
  });

});

5.2 改进后的规范:每个 it 只含一个断言

在 pow-2.view/test.js 中,教程演示了把“pow(2,3)=8、pow(3,4)=81”从同一个 it 里的两个 assert 改写成两个独立的 it:

describe("pow", function() {

  it("2 raised to power 3 is 8", function() {
    assert.equal(pow(2, 3), 8);
  });

  it("3 raised to power 4 is 81", function() {
    assert.equal(pow(3, 4), 81);
  });

});

文中明确解释了两种写法的关键区别:当 assert 触发错误时,it 代码块会立即终止,因此第一种写法中第一个 assert 失败会吞掉第二个 assert 的结果;而“保持测试之间独立,有助于获知代码中正在发生什么”,第二种方式更好。本练习中的三个断言恰好就是这一原则的正面应用场景。

5.3 完整规范的最终形态

pow-full.view/test.js 展示了 BDD 流程收敛后的完整规范:嵌套 describe 分组、makeTest 工厂函数配合 for 循环批量生成用例、以及用 assert.isNaN 覆盖边界条件——每个 it 都只检查一件事:

describe("pow", function() {

  describe("raises x to power 3", function() {

    function makeTest(x) {
      let expected = x * x * x;
      it(`${x} in the power 3 is ${expected}`, function() {
        assert.equal(pow(x, 3), expected);
      });
    }

    for (let x = 1; x <= 5; x++) {
      makeTest(x);
    }

  });

  it("if n is negative, the result is NaN", function() {
    assert.isNaN(pow(2, -1));
  });

  it("if n is not integer, the result is NaN", function() {
    assert.isNaN(pow(2, 1.5));
  });

});

对应的实现 pow-full.view/index.html 中 pow 处理了负数与非整数入参(if (n < 0) return NaN; if (Math.round(n) != n) return NaN;),与测试一一呼应。这印证了 BDD 的核心节奏:先写可能失败的规范,再实现让测试转绿。

5.4 浏览器端测试环境的搭建

如需本地复现,可直接参考 index.html 的五段式页面结构(这也是所有 pow-*.view/index.html 的通用骨架):

  1. <head> 中引入 Mocha 的 CSS 与 JS,并调用 mocha.setup('bdd') 启用 BDD 风格;
  2. 引入 Chai,并将 let assert = chai.assert; 暴露为全局变量;
  3. 页面内 <script> 定义被测函数(如 pow);
  4. 引入测试脚本 test.js(包含 describe/it);
  5. 放置 <div id="mocha"></div> 作为结果输出容器,最后执行 mocha.run() 启动测试。

六、总结:写测试的三条可执行准则

回到本练习,我们可以提炼出三条在编写 Mocha 单元测试时可直接套用的准则:

  1. 一个 it 只检查一个行为:把多个相互独立的断言拆成多个 it,让失败时能立即从标题定位到具体输入与期望值;
  2. 用 describe 分组、用 it 标题做文档:清晰的标题既是可读文档,也是失败报告中的定位线索,这正是 BDD 中“测试、文档、案例三位一体”的体现;
  3. 用 it.only 临时隔离、用“先失败后转绿”驱动开发:需要调试单个用例时用 it.only 缩小范围,但务必在调试结束后还原;新增行为(如 NaN 边界)时先写测试再实现,让规范始终与代码同步演进。

题目中的代码之所以“正确且通过”,是因为它验证的逻辑本身没有问题;它之所以“有错误”,是因为它在测试的可诊断性上犯了设计错误。测试的价值不只在于证明代码此刻正确,更在于当代码将来被修改时,能在几秒钟内精确定位哪里出了问题——这正是自动化测试在现代 JavaScript 项目中的核心价值。

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