异步测试三种模式 - 回调 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)
]);
});
下一步
- chai-as-promised 详细用法 → chai-as-promised
- HTTP 接口测试 → 11-HTTP接口测试
- 异常断言 → 异常捕获