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