Chai 中文文档 下载 App

异步测试三种模式 - 回调 callback、Promise、async/await

核心前提

Chai 的所有断言都是同步的——它不会自动等待 Promise resolve,也不会自动等待回调触发。异步等待的职责由测试运行器(Mocha、Jest、Vitest 等)承担。

所以理解异步测试的关键是:你选择的"异步模式"决定了测试运行器如何知道你的异步操作完成了,然后 Chai 才能在"已经有结果"的前提下做断言。

模式一:回调(Callback / done)

最传统的异步测试方式,配合 Node.js 风格的 callback(err, result):

it('异步获取用户', function (done) {
  // function 有一个参数 → Mocha 判定为异步测试,等待 done()
  getUserById(123, function (err, user) {
    if (err) {
      return done(err);   // 出错了,把错误传给 done 让 Mocha 知道
    }

    // 正常断言
    expect(user.name).to.be.a('string');
    expect(user.id).to.equal(123);

    done();  // 告诉 Mocha:测试完成了
  });
});

Mocha 如何识别异步:function (done) 有参数 → 认为需要等待 done 回调。如果不调用 done(),Mocha 会在默认超时(2 秒)后判定测试失败。

错误传播:done(err) 把异步错误传给 Mocha 做正确的报告,不要自己 throw。

回调模式的陷阱

// ❌ 忘记 done() → 测试挂起直到超时
it('忘记 done', function (done) {
  getUserById(123, function (err, user) {
    expect(user).to.exist;
    // 没有 done() → 测试不会结束
  });
});

// ❌ 在回调里 throw error 而不是 done(err)
it('throw 而不是 done(err)', function (done) {
  getUserById(123, function (err, user) {
    if (err) throw err;   // Mocha 不会捕获这个 throw
  });
});

模式二:Promise return

如果被测函数返回 Promise,直接 return 给测试运行器:

it('异步获取用户(Promise)', function () {
  // Mocha 检测到 return Promise,自动等待
  return getUserById(123).then(function (user) {
    expect(user.name).to.be.a('string');
    expect(user.id).to.equal(123);
  });
});

Mocha 如何识别:it 回调 return 了一个 Promise。

链式多个 Promise

it('先创建再查询', function () {
  return createUser({ name: 'Alice' })
    .then(createdUser => {
      expect(createdUser.id).to.exist;
      return getUserById(createdUser.id);
    })
    .then(fetchedUser => {
      expect(fetchedUser.name).to.equal('Alice');
    });
});

错误场景

Promise reject 会被 Mocha 捕获并报告:

it('拒绝时应该 reject', function () {
  return expect(userService.deleteUser(-1)).to.be.rejected;
  // 但这用的是 chai-as-promised 插件,见下
});

模式三:async / await(推荐)

ES2017 语法,最清爽:

it('异步获取用户(async/await)', async function () {
  const user = await getUserById(123);
  expect(user.name).to.be.a('string');
  expect(user.id).to.equal(123);
});

Mocha 如何识别:async function 返回 Promise,自动等待。

并行 await

it('并行获取多个资源', async function () {
  const [user, orders, settings] = await Promise.all([
    getUserById(123),
    getOrders(123),
    getUserSettings(123)
  ]);

  expect(user).to.exist;
  expect(orders).to.be.an('array');
  expect(settings).to.be.an('object');
});

try-catch 测试异常

it('无效 ID 应该抛错', async function () {
  try {
    await getUserById(-1);
    // 如果走到这里说明没有抛错,主动让测试失败
    throw new Error('应该抛错但没有');
  } catch (err) {
    expect(err).to.be.an.instanceOf(Error);
    expect(err.message).to.include('not found');
  }
});

这种写法可以但比较啰嗦。推荐用 chai-as-promised:

import chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised);

it('无效 ID 应该抛错', async function () {
  await expect(getUserById(-1)).to.be.rejectedWith(Error, 'not found');
});

三种模式对比

模式适用场景代码风格推荐度
回调 done老代码 / Node.js v0.10 风格嵌套回调,缩进深一般
Promise .then需要手动串联多个步骤链式 .then好
async / await新代码 / 大多数场景同步风格代码最推荐

新写的代码直接用 async/await 模式。

与 chai-as-promised 的配合

chai-as-promised 是最常用的异步断言插件,提供 .eventually、.fulfilled、.rejected 等链式语言:

import chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised);

// 验证 Promise resolve 的值
await expect(asyncOperation()).to.eventually.deep.equal(expectedResult);

// 验证 Promise reject
await expect(asyncOperation()).to.be.rejected;
await expect(asyncOperation()).to.be.rejectedWith(Error);
await expect(asyncOperation()).to.be.rejectedWith(Error, 'timeout');

// 配合 .not
await expect(asyncOperation()).to.not.be.rejected;
await expect(asyncOperation()).to.not.be.rejectedWith(NetworkError);

详细用法见 chai-as-promised。

超时设置

Mocha 默认单个测试超时 2000ms。异步测试如果需要更长时间:

// .mocharc.js
module.exports = {
  timeout: 10000  // 全局 10 秒
};

// 或单个测试内设置
it('慢操作', async function () {
  this.timeout(15000);  // 这个测试 15 秒
  await slowOperation();
  expect(result).to.exist;
});

超时太短会导致异步测试被提前终止。

常见陷阱

忘记 return / await

// ❌ 没有 return——Mocha 不知道要等待
it('没有 return', function () {
  getUserById(123).then(user => {
    expect(user).to.exist;  // 这段代码可能测试结束后才执行
  });
});

// ✅ 正确
it('有 return', function () {
  return getUserById(123).then(user => {
    expect(user).to.exist;
  });
});

async 函数里忘了 await

// ❌ expect 的是 Promise 对象本身,永远 truthy
it('忘记 await', async function () {
  expect(getUserById(123)).to.exist;  // Promise 对象存在而已
});

// ✅ 或者 await,或者用 chai-as-promised
it('正确 await', async function () {
  const user = await getUserById(123);
  expect(user).to.exist;
});

多个并发异步操作没有等待全部

// ❌ 只等待了一个
it('多个异步', async function () {
  const user = await getUserById(123);
  createOrder(user.id);  // 没有 await!
  // 测试可能在 createOrder 完成前就结束了
});

// ✅ 用 Promise.all
it('多个异步', async function () {
  const user = await getUserById(123);
  await Promise.all([
    createOrder(user.id),
    sendWelcomeEmail(user.email)
  ]);
});

下一步