Chai 中文文档 下载 App

TDD 工作流 - 用 Chai 驱动测试先行开发

什么是 TDD

测试驱动开发(Test-Driven Development)是一种开发方法论,核心循环:

  1. 红灯(Red):写一个测试,它会失败
  2. 绿灯(Green):写最小量的代码让测试通过
  3. 重构(Refactor):不改变行为的前提下优化代码

Chai 是这个循环中的"红灯检测器"——它告诉你测试是失败(红灯)还是通过(绿灯)。

一个完整的 TDD 例子

需求:写一个计算器模块,支持加、减、乘、除。

第一步:写第一个测试(红灯)

// test/calculator.test.js
const { expect } = require('chai');
const calculator = require('../src/calculator');

describe('Calculator', function () {
  describe('#add()', function () {
    it('1 + 1 应该等于 2', function () {
      expect(calculator.add(1, 1)).to.equal(2);
    });
  });
});

此时 src/calculator.js 还不存在。运行测试:

npm test
// 失败:Cannot find module '../src/calculator'

红灯亮。

第二步:写最小实现(绿灯)

// src/calculator.js
module.exports = {
  add: (a, b) => a + b
};

运行测试:通过。绿灯亮。

第三步:重构(可选)

当前实现没有什么可重构的。继续下一个功能。

第四步:再写一个测试(红灯)

describe('#add()', function () {
  it('负数相加', function () {
    expect(calculator.add(-1, -2)).to.equal(-3);
  });
});

运行测试:通过。绿灯。

第五步:覆盖剩余功能

describe('#subtract()', function () {
  it('减法', function () {
    expect(calculator.subtract(5, 3)).to.equal(2);
  });
});

describe('#divide()', function () {
  it('正常除法', function () {
    expect(calculator.divide(10, 2)).to.equal(5);
  });

  it('除以 0 应该抛错', function () {
    expect(() => calculator.divide(10, 0)).to.throw();
  });
});

每个测试 → 红灯 → 绿灯 → 重构。循环下去。

用 describe / context / it 组织规格

BDD 风格的命名让测试读起来像一份规格说明书:

describe('订单服务', function () {
  context('创建订单', function () {
    context('库存充足', function () {
      it('应该成功创建订单', function () { /* ... */ });
      it('应该扣减库存', function () { /* ... */ });
      it('应该记录操作日志', function () { /* ... */ });
    });

    context('库存不足', function () {
      it('应该返回库存不足错误', function () { /* ... */ });
      it('不应该创建订单记录', function () { /* ... */ });
    });
  });

  context('查询订单', function () {
    it('存在的订单应该返回详情', function () { /* ... */ });
    it('不存在的订单应该返回 null', function () { /* ... */ });
  });
});

Mocha 运行时输出的报告读起来像这样:

  订单服务
    创建订单
      库存充足
        ✓ 应该成功创建订单
        ✓ 应该扣减库存
        ✓ 应该记录操作日志
      库存不足
        ✓ 应该返回库存不足错误
        ✓ 不应该创建订单记录
    查询订单
      ✓ 存在的订单应该返回详情
      ✓ 不存在的订单应该返回 null

这就是"规格说明"的真正样子——不仅描述系统应该做什么,还能自动执行、自动验证。

何时用 TDD

何时不用严格 TDD

但"不用严格 TDD"不等于"不写测试"。测试应该在开发过程中自然生长,而不是等开发完才补。

Chai 断言在 TDD 中的角色

Chai 的断言就是"红灯检测器":

// 这条断言 = "这个功能应该 X"
// 如果不通过 → 红灯(代码还没实现或实现错了)
// 如果通过 → 绿灯(行为正确)
expect(result).to.equal(expected);

assert 风格和 BDD 风格都能驱动 TDD,选哪个都行。关键是测试先写、实现后写、红转绿。

让 .throw 精确到错误类型

错误处理也是 TDD 应该覆盖的行为:

// 红灯
expect(() => calculator.divide(10, 0)).to.throw(DivideByZeroError, '除数不能为零');

这定义了"除以 0 时应该抛 DivideByZeroError 且消息包含'除数不能为零'"。实现:

class DivideByZeroError extends Error {
  constructor() {
    super('除数不能为零');
    this.name = 'DivideByZeroError';
  }
}

function divide(a, b) {
  if (b === 0) throw new DivideByZeroError();
  return a / b;
}

重构阶段

全部测试通过后,重构阶段的任务是让代码更好但保持所有测试仍然绿色:

重构完跑一遍测试——全绿就说明行为没变。

下一步