Mocha 异步测试 #
异步测试概述 #
JavaScript 中大量使用异步操作,Mocha 提供了多种方式来测试异步代码:
text
┌─────────────────────────────────────────────────────────────┐
│ 异步测试方式 │
├─────────────────────────────────────────────────────────────┤
│ 1. 回调函数 (done) │
│ 2. Promise 返回 │
│ 3. async/await │
│ 4. chai-as-promised 插件 │
└─────────────────────────────────────────────────────────────┘
回调方式 #
使用 done 参数 #
当测试函数接收 done 参数时,Mocha 会等待调用 done() 才结束测试:
javascript
describe('Callback Style', function() {
it('should complete async operation', function(done) {
setTimeout(function() {
assert.ok(true);
done(); // 通知 Mocha 测试完成
}, 100);
});
it('should handle async error', function(done) {
asyncOperation(function(err, result) {
if (err) {
done(err); // 传递错误
return;
}
assert.equal(result, 'expected');
done();
});
});
});
错误处理 #
javascript
describe('Error Handling', function() {
it('should catch async error', function(done) {
function asyncFunction(callback) {
setTimeout(function() {
callback(new Error('Something went wrong'));
}, 100);
}
asyncFunction(function(err, result) {
try {
expect(err).to.exist;
expect(err.message).to.equal('Something went wrong');
done();
} catch (e) {
done(e);
}
});
});
// 更简洁的错误处理
it('should handle error simply', function(done) {
asyncFunction(function(err) {
if (err) {
return done(err);
}
done();
});
});
});
回调方式的注意事项 #
javascript
describe('Callback Pitfalls', function() {
// ❌ 错误:忘记调用 done
it('wrong: forgot done', function(done) {
setTimeout(function() {
assert.ok(true);
// 忘记调用 done(),测试会超时
}, 100);
});
// ❌ 错误:多次调用 done
it('wrong: multiple done calls', function(done) {
setTimeout(function() {
done();
}, 100);
setTimeout(function() {
done(); // Error: done() called multiple times
}, 200);
});
// ✅ 正确:确保只调用一次 done
it('correct: single done call', function(done) {
let called = false;
setTimeout(function() {
if (!called) {
called = true;
done();
}
}, 100);
});
});
Promise 方式 #
返回 Promise #
当测试返回 Promise 时,Mocha 会自动等待 Promise 完成:
javascript
describe('Promise Style', function() {
it('should resolve', function() {
return Promise.resolve(1)
.then(function(value) {
expect(value).to.equal(1);
});
});
it('should chain promises', function() {
return fetchUser(1)
.then(function(user) {
expect(user.name).to.equal('John');
return fetchOrders(user.id);
})
.then(function(orders) {
expect(orders).to.have.length.above(0);
});
});
});
测试 Promise 拒绝 #
javascript
describe('Promise Rejection', function() {
// 测试 Promise 拒绝
it('should reject', function() {
return Promise.reject(new Error('failed'))
.then(function() {
throw new Error('Should not resolve');
})
.catch(function(err) {
expect(err.message).to.equal('failed');
});
});
// 使用 chai-as-promised(推荐)
it('should reject with chai-as-promised', function() {
return expect(Promise.reject(new Error('failed')))
.to.be.rejectedWith('failed');
});
});
async/await 方式 #
基本用法 #
javascript
describe('async/await', function() {
it('should work with async/await', async function() {
const user = await fetchUser(1);
expect(user.name).to.equal('John');
});
it('should handle multiple awaits', async function() {
const user = await fetchUser(1);
const orders = await fetchOrders(user.id);
expect(orders).to.have.length.above(0);
});
it('should handle parallel operations', async function() {
const [users, products] = await Promise.all([
fetchUsers(),
fetchProducts()
]);
expect(users).to.be.an('array');
expect(products).to.be.an('array');
});
});
错误处理 #
javascript
describe('async/await Error Handling', function() {
it('should catch error with try/catch', async function() {
try {
await fetchUser(-1);
throw new Error('Should have thrown');
} catch (err) {
expect(err.message).to.equal('User not found');
}
});
// 使用 chai-as-promised(推荐)
it('should reject with chai-as-promised', async function() {
await expect(fetchUser(-1))
.to.be.rejectedWith('User not found');
});
// 测试异步函数抛出错误
it('should throw error', async function() {
await expect((async () => {
throw new Error('test error');
})()).to.be.rejectedWith('test error');
});
});
async/await 与钩子 #
javascript
describe('async/await with hooks', function() {
let db;
before(async function() {
db = await connectDatabase();
});
after(async function() {
await db.close();
});
beforeEach(async function() {
await db.clear();
});
it('should insert user', async function() {
await db.insert('users', { name: 'John' });
const users = await db.findAll('users');
expect(users).to.have.lengthOf(1);
});
});
chai-as-promised 插件 #
安装 #
bash
npm install --save-dev chai-as-promised
配置 #
javascript
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
const { expect } = chai;
基本用法 #
javascript
describe('chai-as-promised', function() {
// 测试 Promise 解决
it('should resolve', function() {
return expect(Promise.resolve(1)).to.eventually.equal(1);
});
// 测试 Promise 拒绝
it('should reject', function() {
return expect(Promise.reject(new Error('fail')))
.to.be.rejectedWith('fail');
});
// 测试 Promise 类型
it('should be fulfilled', function() {
return expect(Promise.resolve('ok')).to.be.fulfilled;
});
it('should be rejected', function() {
return expect(Promise.reject('error')).to.be.rejected;
});
});
高级用法 #
javascript
describe('chai-as-promised Advanced', function() {
// 链式断言
it('should chain assertions', function() {
return expect(Promise.resolve({ name: 'John' }))
.to.eventually.have.property('name')
.that.equals('John');
});
// 测试拒绝的错误类型
it('should reject with specific error type', function() {
return expect(Promise.reject(new TypeError('type error')))
.to.be.rejectedWith(TypeError);
});
// 测试拒绝的错误属性
it('should reject with error having property', function() {
const error = new Error('custom error');
error.code = 'CUSTOM_ERROR';
return expect(Promise.reject(error))
.to.be.rejected.with.property('code', 'CUSTOM_ERROR');
});
// 使用 async/await
it('should work with async/await', async function() {
await expect(Promise.resolve(1)).to.eventually.equal(1);
await expect(Promise.reject('error')).to.be.rejected;
});
});
notified 方法 #
javascript
describe('notified', function() {
it('should notify on resolution', function() {
return expect(Promise.resolve(1))
.to.eventually.equal(1)
.and.notify(done);
});
it('should notify on rejection', function() {
return expect(Promise.reject(new Error('fail')))
.to.be.rejectedWith('fail')
.and.notify(done);
});
});
超时处理 #
设置超时 #
javascript
describe('Timeout Handling', function() {
// 设置套件级别超时
this.timeout(5000);
it('should complete within timeout', function(done) {
this.timeout(3000); // 测试级别超时
setTimeout(done, 2500);
});
it('async/await with timeout', async function() {
this.timeout(5000);
await slowOperation();
});
// Promise 超时
it('promise with timeout', function() {
this.timeout(5000);
return slowPromise();
});
});
超时错误处理 #
javascript
describe('Timeout Errors', function() {
it('should handle timeout', function(done) {
this.timeout(100);
// 使用 Promise.race 处理超时
Promise.race([
slowOperation(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 50)
)
]).then(done).catch(done);
});
});
实际应用示例 #
测试 HTTP 请求 #
javascript
const request = require('supertest');
const app = require('../app');
describe('API Tests', function() {
it('should get users', function() {
return request(app)
.get('/api/users')
.expect(200)
.then(function(res) {
expect(res.body).to.be.an('array');
});
});
it('should create user', async function() {
const res = await request(app)
.post('/api/users')
.send({ name: 'John', email: 'john@example.com' })
.expect(201);
expect(res.body).to.have.property('id');
expect(res.body.name).to.equal('John');
});
it('should handle 404', async function() {
const res = await request(app)
.get('/api/users/999')
.expect(404);
expect(res.body.error).to.equal('User not found');
});
});
测试数据库操作 #
javascript
describe('Database Operations', function() {
let db;
before(async function() {
db = await connectDatabase();
});
after(async function() {
await db.close();
});
beforeEach(async function() {
await db.query('DELETE FROM users');
});
it('should insert user', async function() {
const result = await db.query(
'INSERT INTO users (name, email) VALUES (?, ?)',
['John', 'john@example.com']
);
expect(result.affectedRows).to.equal(1);
});
it('should find user', async function() {
await db.query(
'INSERT INTO users (name, email) VALUES (?, ?)',
['Jane', 'jane@example.com']
);
const users = await db.query(
'SELECT * FROM users WHERE name = ?',
['Jane']
);
expect(users).to.have.lengthOf(1);
expect(users[0].email).to.equal('jane@example.com');
});
it('should handle transaction', async function() {
await db.beginTransaction();
try {
await db.query('INSERT INTO users (name) VALUES (?)', ['User1']);
await db.query('INSERT INTO users (name) VALUES (?)', ['User2']);
await db.commit();
} catch (err) {
await db.rollback();
throw err;
}
const users = await db.query('SELECT * FROM users');
expect(users).to.have.lengthOf(2);
});
});
测试文件操作 #
javascript
const fs = require('fs').promises;
const path = require('path');
describe('File Operations', function() {
const testFile = path.join(__dirname, 'test-file.txt');
afterEach(async function() {
try {
await fs.unlink(testFile);
} catch (err) {
// 文件不存在,忽略错误
}
});
it('should write and read file', async function() {
await fs.writeFile(testFile, 'Hello, World!');
const content = await fs.readFile(testFile, 'utf8');
expect(content).to.equal('Hello, World!');
});
it('should handle file not found', async function() {
await expect(fs.readFile('nonexistent.txt'))
.to.be.rejected;
});
it('should check file existence', async function() {
await fs.writeFile(testFile, 'test');
const exists = await fs.access(testFile)
.then(() => true)
.catch(() => false);
expect(exists).to.be.true;
});
});
测试定时器 #
javascript
describe('Timer Tests', function() {
let clock;
beforeEach(function() {
clock = sinon.useFakeTimers();
});
afterEach(function() {
clock.restore();
});
it('should call callback after delay', function() {
const callback = sinon.spy();
setTimeout(callback, 1000);
clock.tick(999);
expect(callback.called).to.be.false;
clock.tick(1);
expect(callback.called).to.be.true;
});
it('should work with async/await', async function() {
let resolved = false;
const promise = new Promise(resolve => {
setTimeout(() => {
resolved = true;
resolve();
}, 1000);
});
clock.tick(1000);
await promise;
expect(resolved).to.be.true;
});
});
最佳实践 #
1. 优先使用 async/await #
javascript
// ✅ 推荐:async/await
it('should fetch user', async function() {
const user = await fetchUser(1);
expect(user.name).to.equal('John');
});
// ⚠️ 可用但不够简洁:Promise
it('should fetch user', function() {
return fetchUser(1).then(user => {
expect(user.name).to.equal('John');
});
});
// ⚠️ 可用但容易出错:回调
it('should fetch user', function(done) {
fetchUser(1, function(err, user) {
if (err) return done(err);
expect(user.name).to.equal('John');
done();
});
});
2. 合理设置超时 #
javascript
describe('API Tests', function() {
// API 测试可能较慢
this.timeout(10000);
it('should handle slow API', async function() {
const result = await slowApiCall();
expect(result).to.exist;
});
});
3. 错误处理要完整 #
javascript
// ✅ 完整的错误处理
it('should handle all error cases', async function() {
await expect(fetchUser(-1))
.to.be.rejectedWith('Invalid user ID');
await expect(fetchUser(null))
.to.be.rejectedWith('User ID is required');
});
4. 并行与串行 #
javascript
describe('Parallel vs Serial', function() {
// 并行执行
it('should run in parallel', async function() {
const [users, products, orders] = await Promise.all([
fetchUsers(),
fetchProducts(),
fetchOrders()
]);
expect(users).to.exist;
expect(products).to.exist;
expect(orders).to.exist;
});
// 串行执行(有依赖关系)
it('should run in serial', async function() {
const user = await createUser({ name: 'John' });
const order = await createOrder({ userId: user.id });
const payment = await processPayment(order.id);
expect(payment.status).to.equal('completed');
});
});
下一步 #
现在你已经掌握了异步测试的方法,接下来学习 Mock 和 Stub 了解如何模拟依赖!
最后更新:2026-03-28