Chai 中文文档 下载 App

chai-as-promised 插件详解 - eventually fulfilled rejected 异步断言

为什么需要这个插件

异步测试的核心痛点是:Promise 不会抛出同步异常。常规的 expect(promise).to.throw() 无法捕获 Promise rejection,因为 Promise 的 reject 是异步事件。

之前的解决方案(async/await + try-catch)写起来啰嗦:

// 不用 chai-as-promised:啰嗦
let error;
try {
  await asyncOperation();
} catch (e) {
  error = e;
}
expect(error).to.be.an.instanceOf(Error);

chai-as-promised 把它简化成:

await expect(asyncOperation()).to.be.rejectedWith(Error);

安装

npm install chai-as-promised --save-dev

Node.js:

import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';

chai.use(chaiAsPromised);

ESM + TypeScript 可能需要默认导入:

import chaiAsPromised from 'chai-as-promised';

v7+ 版本对 ESM 支持更好。如果遇到导入问题,先确认版本。

核心 API 一览

断言语义返回
.eventually把后续断言应用到 Promise resolve 的值Promise
.fulfilled / .be.fulfilledPromise 应该 resolve(reject 会让测试失败)Promise
.rejected / .be.rejectedPromise 应该 reject(resolve 会让测试失败)Promise
.rejectedWith(ErrorType)Promise reject 指定类型错误Promise
.rejectedWith(ErrorType, message)类型 + 消息(字符串或正则)Promise
.become(value)同 .eventually.deep.equal(value)Promise
.notify(done)对接老式回调风格的兼容写法无

.eventually —— resolve 值的断言

这是最常用的链式修饰器——把后面的普通 Chai 断言延迟应用到 Promise resolve 出来的值:

// 不带 chai-as-promised:验证同步值
expect(result).to.deep.equal({ name: 'Alice' });

// 带 chai-as-promised:验证 Promise resolve 的值
await expect(asyncOperation()).to.eventually.deep.equal({ name: 'Alice' });

.eventually 后面可以跟任何 Chai 断言——.equal、.include、.property、.members、.throw(对 resolve 值本身调用)等等。

链式组合

await expect(getUser(123)).to.eventually
  .be.an('object')
  .that.has.property('id', 123)
  .and.have.property('name')
  .that.is.a('string');

不支持 .not 的场景

链式中可以加 .not,但要注意:.not 只能出现在 .eventually 之后:

// ✅ 正确
await expect(asyncOperation()).to.eventually.not.throw();

// ❌ 错误——.not 在 .eventually 之前没意义
await expect(asyncOperation()).to.not.eventually.deep.equal(x);

.fulfilled —— Promise 应该 resolve

// 只要 resolve 就行,不关心值
await expect(asyncOperation()).to.be.fulfilled;
await expect(asyncOperation()).to.not.be.rejected;

.rejected / .rejectedWith —— Promise 应该 reject

// 只要 reject 就行,不关心错误类型
await expect(asyncOperation()).to.be.rejected;

// 指定错误类型
await expect(asyncOperation()).to.be.rejectedWith(ValidationError);

// 指定错误类型 + 消息(包含匹配)
await expect(asyncOperation()).to.be.rejectedWith(ValidationError, 'invalid email');

// 消息可以是正则
await expect(asyncOperation()).to.be.rejectedWith(/timeout/i);

.become(value) —— eventually.deep.equal 的简写

// 这两条等价
await expect(promise).to.eventually.deep.equal(expected);
await expect(promise).to.become(expected);

.become 内部就是 .eventually.deep.equal。如果你需要深度匹配,become 确实更简洁。

必须 await / return

这是最重要的纪律——chai-as-promised 的所有断言返回 Promise,必须 await 或 return 给测试运行器:

// ❌ 忘记 await——测试在 Promise resolve 之前就结束了
it('忘记 await', async function () {
  expect(asyncOperation()).to.eventually.equal(42);
  // 测试函数同步返回,Mocha 认为测试结束
  // 断言还没执行!
});

// ✅ 正确:await
it('正确 await', async function () {
  await expect(asyncOperation()).to.eventually.equal(42);
});

// ✅ 正确:return
it('正确 return', function () {
  return expect(asyncOperation()).to.eventually.equal(42);
});

没有 await / return = 测试静默跳过——这是 chai-as-promised 最常见的坑。

传入已 resolve 的值

如果某个场景不是异步的,就是一个同步值,但你想用 chai-as-promised 的链式:

// 直接传 Promise.resolve(value)
await expect(Promise.resolve(42)).to.eventually.equal(42);

不过这没必要——直接用普通 expect 风格就行。chai-as-promised 专门用于 值在未来才可用 的场景。

错误消息

rejectedWith 失败时的错误消息:

// 期望 reject 但 resolve 了
await expect(Promise.resolve(42)).to.be.rejectedWith(Error);
// AssertionError: expected promise to be rejected with Error but it was fulfilled with 42

// 期望 resolve 但 reject 了
await expect(Promise.reject(new Error('boom'))).to.eventually.equal(42);
// Error: boom

错误消息通常很有信息量。

常见陷阱

忘记 await / return

上面详细讲过,这是第一大坑。

插件在其他插件之后注册

// ❌ 注册顺序可能影响行为
chai.use(otherPlugin);
chai.use(chaiAsPromised);  // 某些断言可能没被 promisify

// ✅ 建议 chai-as-promised 最先注册
chai.use(chaiAsPromised);
chai.use(otherPlugin);

chai-as-promised 安装时会对当前已注册的所有 Chai 断言做 promisify 包装。如果后续有新插件注册的断言没被包到,它们在 .eventually 里可能行为异常。

测试运行器没有正确等待

某些测试运行器默认不等待 async 函数的 Promise。Mocha(v3+)原生支持,Jest 原生支持。如果用了比较老的运行器,确认它是否支持 async/await。

setTimeout 等定时操作

// ❌ setTimeout 里的异步不受控制
it('setTimeout', async function () {
  setTimeout(() => doSomething(), 100);
  // 测试可能在 100ms 之前就结束了
});

// ✅ 用 Promise + delay 包装
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
await delay(100);
// 然后断言

与 Node.js assert 的对比

Node.js 内置 assert 不支持异步断言。要在 Node assert 风格里断言 Promise,必须自己写:

// Node 原生 assert
const assert = require('assert');

async function test() {
  try {
    await asyncOperation();
    assert.fail('应该 reject');
  } catch (e) {
    assert.ok(e instanceof ValidationError);
  }
}

chai-as-promised 的写法更简洁直观。

下一步