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