Mocha 生命周期钩子 #

钩子概述 #

Mocha 提供四种生命周期钩子,用于在测试前后执行代码:

text
┌─────────────────────────────────────────────────────────────┐
│                    测试执行顺序                              │
├─────────────────────────────────────────────────────────────┤
│  before()        ─── 所有测试之前执行一次                    │
│      │                                                      │
│      ├── beforeEach()  ─── 每个测试之前执行                  │
│      │       │                                              │
│      │       ├── it('test 1')                               │
│      │       │                                              │
│      │       └── afterEach()  ─── 每个测试之后执行           │
│      │                                                      │
│      ├── beforeEach()                                       │
│      │       │                                              │
│      │       ├── it('test 2')                               │
│      │       │                                              │
│      │       └── afterEach()                                │
│      │                                                      │
│  after()         ─── 所有测试之后执行一次                    │
└─────────────────────────────────────────────────────────────┘

四种钩子 #

before #

在所有测试之前执行一次:

javascript
describe('Database Tests', function() {
  let db;

  before(async function() {
    // 建立数据库连接
    db = await connectDatabase();
    console.log('Database connected');
  });

  it('should query users', async function() {
    const users = await db.query('SELECT * FROM users');
    expect(users).to.be.an('array');
  });

  it('should insert user', async function() {
    const result = await db.insert('users', { name: 'John' });
    expect(result.affectedRows).to.equal(1);
  });
});

after #

在所有测试之后执行一次:

javascript
describe('Database Tests', function() {
  let db;

  before(async function() {
    db = await connectDatabase();
  });

  after(async function() {
    // 关闭数据库连接
    await db.close();
    console.log('Database closed');
  });

  it('test 1', function() { /* ... */ });
  it('test 2', function() { /* ... */ });
});

beforeEach #

在每个测试之前执行:

javascript
describe('User Tests', function() {
  let user;

  beforeEach(function() {
    // 每个测试都创建新的用户实例
    user = new User('John', 'john@example.com');
  });

  it('should have correct name', function() {
    expect(user.name).to.equal('John');
  });

  it('should update name', function() {
    user.setName('Jane');
    expect(user.name).to.equal('Jane');
    // 其他测试不受影响
  });

  it('should still have original name', function() {
    expect(user.name).to.equal('John');
  });
});

afterEach #

在每个测试之后执行:

javascript
describe('File Tests', function() {
  let tempFile;

  beforeEach(function() {
    tempFile = createTempFile('test.txt');
  });

  afterEach(function() {
    // 清理临时文件
    deleteFile(tempFile);
  });

  it('should read file', function() {
    const content = readFile(tempFile);
    expect(content).to.exist;
  });

  it('should write file', function() {
    writeFile(tempFile, 'hello');
    const content = readFile(tempFile);
    expect(content).to.equal('hello');
  });
});

完整示例 #

javascript
const { expect } = require('chai');
const Database = require('./database');
const User = require('./user');

describe('User Integration Tests', function() {
  let db;
  let user;

  // 所有测试之前执行一次
  before(async function() {
    console.log('=== Starting test suite ===');
    db = new Database('test-db');
    await db.connect();
  });

  // 所有测试之后执行一次
  after(async function() {
    await db.disconnect();
    console.log('=== Test suite finished ===');
  });

  // 每个测试之前执行
  beforeEach(async function() {
    user = new User(db);
    await user.create({ name: 'John', email: 'john@test.com' });
  });

  // 每个测试之后执行
  afterEach(async function() {
    await user.delete();
  });

  describe('find operations', function() {
    it('should find user by id', async function() {
      const found = await User.findById(user.id);
      expect(found).to.exist;
      expect(found.name).to.equal('John');
    });

    it('should find user by email', async function() {
      const found = await User.findByEmail('john@test.com');
      expect(found).to.exist;
    });
  });

  describe('update operations', function() {
    it('should update user name', async function() {
      await user.update({ name: 'Jane' });
      const updated = await User.findById(user.id);
      expect(updated.name).to.equal('Jane');
    });
  });
});

嵌套钩子 #

钩子可以嵌套使用:

javascript
describe('Outer', function() {
  before(function() {
    console.log('outer before');
  });

  after(function() {
    console.log('outer after');
  });

  beforeEach(function() {
    console.log('outer beforeEach');
  });

  afterEach(function() {
    console.log('outer afterEach');
  });

  describe('Inner 1', function() {
    before(function() {
      console.log('inner 1 before');
    });

    after(function() {
      console.log('inner 1 after');
    });

    beforeEach(function() {
      console.log('inner 1 beforeEach');
    });

    afterEach(function() {
      console.log('inner 1 afterEach');
    });

    it('test 1', function() {
      console.log('test 1');
    });
  });

  describe('Inner 2', function() {
    it('test 2', function() {
      console.log('test 2');
    });
  });
});

// 执行顺序:
// outer before
// inner 1 before
// outer beforeEach
// inner 1 beforeEach
// test 1
// inner 1 afterEach
// outer afterEach
// outer beforeEach
// test 2
// outer afterEach
// inner 1 after
// outer after

异步钩子 #

回调方式 #

javascript
describe('Async Hooks', function() {
  let db;

  before(function(done) {
    db = new Database();
    db.connect(function(err) {
      if (err) return done(err);
      done();
    });
  });

  after(function(done) {
    db.disconnect(done);
  });

  it('test', function() {
    expect(db.isConnected()).to.be.true;
  });
});

Promise 方式 #

javascript
describe('Promise Hooks', function() {
  let db;

  before(function() {
    db = new Database();
    return db.connect(); // 返回 Promise
  });

  after(function() {
    return db.disconnect();
  });

  it('test', function() {
    expect(db.isConnected()).to.be.true;
  });
});

async/await 方式 #

javascript
describe('Async/Await Hooks', function() {
  let db;

  before(async function() {
    db = new Database();
    await db.connect();
    console.log('Database connected');
  });

  after(async function() {
    await db.disconnect();
    console.log('Database disconnected');
  });

  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);
  });

  it('should find users', async function() {
    await db.insert('users', { name: 'Jane' });
    const users = await db.findAll('users');
    expect(users).to.have.lengthOf(1);
  });
});

钩子超时 #

javascript
describe('Hooks Timeout', function() {
  before(function(done) {
    this.timeout(5000); // 设置 5 秒超时
    setTimeout(done, 4000);
  });

  beforeEach(function() {
    this.timeout(1000);
  });

  it('test', function() {
    // ...
  });
});

钩子上下文 #

钩子和测试共享 this 上下文:

javascript
describe('Shared Context', function() {
  before(function() {
    // 在 this 上设置属性
    this.sharedData = { value: 100 };
  });

  beforeEach(function() {
    this.testData = { count: 0 };
  });

  it('can access shared data', function() {
    expect(this.sharedData.value).to.equal(100);
    expect(this.testData.count).to.equal(0);
  });

  it('can modify test data', function() {
    this.testData.count++;
    expect(this.testData.count).to.equal(1);
  });

  it('test data is reset', function() {
    // beforeEach 会重新创建 testData
    expect(this.testData.count).to.equal(0);
  });
});

钩子最佳实践 #

1. 测试隔离 #

每个测试应该独立,不依赖其他测试:

javascript
// ✅ 好的做法
describe('User', function() {
  let user;

  beforeEach(function() {
    user = new User('John'); // 每个测试都有新的 user
  });

  it('should have name', function() {
    expect(user.name).to.equal('John');
  });

  it('should update name', function() {
    user.setName('Jane');
    expect(user.name).to.equal('Jane');
    // 不影响其他测试
  });
});

// ❌ 不好的做法
describe('User', function() {
  let user = new User('John');

  it('should have name', function() {
    expect(user.name).to.equal('John');
  });

  it('should update name', function() {
    user.setName('Jane');
    expect(user.name).to.equal('Jane');
  });

  it('test depends on previous test', function() {
    // 这个测试依赖上一个测试修改了 user.name
    expect(user.name).to.equal('Jane'); // 可能失败
  });
});

2. 资源清理 #

确保在 afterEach 或 after 中清理资源:

javascript
describe('File Operations', function() {
  let tempFiles = [];

  beforeEach(function() {
    const file = createTempFile();
    tempFiles.push(file);
    return file;
  });

  afterEach(function() {
    // 清理所有临时文件
    tempFiles.forEach(file => deleteFile(file));
    tempFiles = [];
  });

  it('test 1', function() { /* ... */ });
  it('test 2', function() { /* ... */ });
});

3. 错误处理 #

钩子中的错误会导致测试失败:

javascript
describe('Error Handling', function() {
  before(function(done) {
    db.connect(function(err) {
      if (err) {
        // 返回错误会跳过所有测试
        done(err);
        return;
      }
      done();
    });
  });

  it('will be skipped if before fails', function() {
    // 如果 before 失败,这个测试不会执行
  });
});

4. 条件跳过 #

javascript
describe('Conditional Tests', function() {
  before(function() {
    if (!process.env.DATABASE_URL) {
      // 跳过整个测试套件
      this.skip();
    }
  });

  it('requires database', function() {
    // 如果没有数据库连接,这个测试会被跳过
  });
});

根级钩子 #

根级钩子应用于所有测试:

javascript
// 在测试文件顶层定义
beforeEach(function() {
  console.log('runs before every test in all files');
});

afterEach(function() {
  console.log('runs after every test in all files');
});

describe('Suite 1', function() {
  it('test 1', function() { /* ... */ });
});

describe('Suite 2', function() {
  it('test 2', function() { /* ... */ });
});

在单独文件中定义根钩子 #

javascript
// test/hooks.js
beforeEach(function() {
  console.log('global beforeEach');
});

afterEach(function() {
  console.log('global afterEach');
});
bash
# 运行时包含根钩子文件
mocha test/hooks.js test/*.test.js

延迟根钩子 #

Mocha 8+ 支持延迟根钩子:

javascript
// 延迟根钩子在所有文件加载后执行
before(function() {
  console.log('runs after all files are loaded');
});

钩子执行顺序示例 #

javascript
describe('A', function() {
  before(function() { console.log('A before'); });
  after(function() { console.log('A after'); });
  beforeEach(function() { console.log('A beforeEach'); });
  afterEach(function() { console.log('A afterEach'); });

  describe('B', function() {
    before(function() { console.log('B before'); });
    after(function() { console.log('B after'); });
    beforeEach(function() { console.log('B beforeEach'); });
    afterEach(function() { console.log('B afterEach'); });

    it('test 1', function() { console.log('test 1'); });
    it('test 2', function() { console.log('test 2'); });
  });
});

// 输出顺序:
// A before
// B before
// A beforeEach
// B beforeEach
// test 1
// B afterEach
// A afterEach
// A beforeEach
// B beforeEach
// test 2
// B afterEach
// A afterEach
// B after
// A after

实用模式 #

数据库测试模式 #

javascript
describe('Database Tests', function() {
  let db;

  before(async function() {
    db = new Database(process.env.TEST_DB_URL);
    await db.connect();
  });

  after(async function() {
    await db.disconnect();
  });

  beforeEach(async function() {
    await db.begin();
  });

  afterEach(async function() {
    await db.rollback();
  });

  it('should insert data', async function() {
    await db.insert('users', { name: 'John' });
    const users = await db.findAll('users');
    expect(users).to.have.lengthOf(1);
  });

  it('should not see previous data', async function() {
    // 因为每个测试后 rollback
    const users = await db.findAll('users');
    expect(users).to.have.lengthOf(0);
  });
});

API 测试模式 #

javascript
const request = require('supertest');
const app = require('../app');

describe('API Tests', function() {
  let server;
  let auth;

  before(function(done) {
    server = app.listen(0, done);
  });

  after(function(done) {
    server.close(done);
  });

  beforeEach(async function() {
    // 每个测试前登录获取 token
    const res = await request(app)
      .post('/auth/login')
      .send({ username: 'test', password: 'test' });
    auth = res.body.token;
  });

  afterEach(function() {
    auth = null;
  });

  it('should access protected route', function() {
    return request(app)
      .get('/api/protected')
      .set('Authorization', `Bearer ${auth}`)
      .expect(200);
  });
});

临时文件模式 #

javascript
const fs = require('fs');
const path = require('path');
const os = require('os');

describe('File Processing', function() {
  let tempDir;

  before(function() {
    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
  });

  after(function() {
    fs.rmSync(tempDir, { recursive: true, force: true });
  });

  beforeEach(function() {
    // 每个测试前清空目录
    const files = fs.readdirSync(tempDir);
    files.forEach(file => {
      fs.unlinkSync(path.join(tempDir, file));
    });
  });

  it('should create file', function() {
    const filePath = path.join(tempDir, 'test.txt');
    fs.writeFileSync(filePath, 'hello');
    expect(fs.existsSync(filePath)).to.be.true;
  });
});

下一步 #

现在你已经掌握了生命周期钩子的使用方法,接下来学习 异步测试 了解如何测试异步代码!

最后更新:2026-03-28