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.fulfilled | Promise 应该 resolve(reject 会让测试失败) | Promise |
.rejected / .be.rejected | Promise 应该 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 的写法更简洁直观。
下一步
- 异步测试三种模式 → 异步测试
- HTTP 接口测试 → chai-http 插件
- 插件基础 → 插件基础