Mocha 高级特性 #
浏览器测试 #
设置浏览器环境 #
Mocha 可以直接在浏览器中运行测试:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mocha Tests</title>
<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css">
</head>
<body>
<div id="mocha"></div>
<!-- 加载 Mocha -->
<script src="https://unpkg.com/mocha/mocha.js"></script>
<!-- 加载断言库 -->
<script src="https://unpkg.com/chai/chai.js"></script>
<!-- 配置 Mocha -->
<script>
mocha.setup('bdd');
var expect = chai.expect;
</script>
<!-- 加载被测试代码 -->
<script src="src/math.js"></script>
<!-- 加载测试文件 -->
<script src="test/math.test.js"></script>
<!-- 运行测试 -->
<script>
mocha.run();
</script>
</body>
</html>
浏览器测试示例 #
javascript
// src/math.js
var MathUtils = {
add: function(a, b) {
return a + b;
},
subtract: function(a, b) {
return a - b;
},
multiply: function(a, b) {
return a * b;
},
divide: function(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
};
// test/math.test.js
describe('MathUtils', function() {
describe('add', function() {
it('should add two numbers', function() {
expect(MathUtils.add(1, 2)).to.equal(3);
});
it('should handle negative numbers', function() {
expect(MathUtils.add(-1, -2)).to.equal(-3);
});
});
describe('divide', function() {
it('should throw error when dividing by zero', function() {
expect(function() {
MathUtils.divide(1, 0);
}).to.throw('Division by zero');
});
});
});
使用 ES 模块 #
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mocha ES Module Tests</title>
<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css">
</head>
<body>
<div id="mocha"></div>
<script type="module">
import { mocha } from 'https://unpkg.com/mocha/mocha.js';
import { expect } from 'https://unpkg.com/chai/chai.js';
import { add, subtract } from './src/math.module.js';
mocha.setup('bdd');
describe('Math', function() {
it('should add', function() {
expect(add(1, 2)).to.equal(3);
});
});
mocha.run();
</script>
</body>
</html>
延迟根套件 #
使用 --delay #
延迟根套件允许在运行测试前执行异步操作:
bash
mocha --delay
javascript
// test/delayed.test.js
setTimeout(function() {
// 异步初始化完成后再运行测试
describe('Delayed Tests', function() {
it('should run after delay', function() {
expect(true).to.be.true;
});
});
// 必须调用 run() 开始测试
run();
}, 1000);
实际应用 #
javascript
// test/delayed.test.js
const Database = require('../src/database');
let db;
setTimeout(async function() {
try {
// 异步初始化数据库
db = await Database.connect('test-db');
describe('Database Tests', function() {
before(function() {
// db 已经初始化
});
it('should query users', async function() {
const users = await db.query('SELECT * FROM users');
expect(users).to.be.an('array');
});
});
run();
} catch (err) {
console.error('Setup failed:', err);
process.exit(1);
}
}, 0);
全局变量 #
定义全局变量 #
javascript
// .mocharc.js
module.exports = {
global: ['myGlobal', 'anotherGlobal']
};
javascript
// test/setup.js
global.myGlobal = {
config: {
apiUrl: 'https://api.example.com'
}
};
global.anotherGlobal = function() {
return 'hello';
};
使用全局变量 #
javascript
// test/api.test.js
describe('API', function() {
it('should use global config', function() {
const url = myGlobal.config.apiUrl;
expect(url).to.equal('https://api.example.com');
});
it('should call global function', function() {
expect(anotherGlobal()).to.equal('hello');
});
});
自定义接口 #
创建自定义接口 #
javascript
// custom-interface.js
module.exports = function(suite) {
suite.on('pre-require', function(context, file, mocha) {
// 自定义 test 函数
context.testCase = function(title, fn) {
context.it(title, fn);
};
// 自定义 describe 函数
context.feature = function(title, fn) {
context.describe(title, fn);
};
// 自定义断言
context.assert = function(actual, expected, message) {
if (actual !== expected) {
throw new Error(message || `Expected ${expected} but got ${actual}`);
}
};
});
};
使用自定义接口 #
bash
mocha --ui ./custom-interface.js test/
javascript
// test/custom.test.js
feature('User Management', function() {
testCase('should create user', function() {
assert(1 + 1, 2, 'Math is broken');
});
});
测试调试 #
使用 Node.js 调试器 #
bash
# 启用调试模式
mocha --inspect test/
# 等待调试器连接
mocha --inspect-brk test/
# 使用 Chrome DevTools
# chrome://inspect
使用 VS Code 调试 #
json
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Mocha Tests",
"program": "${workspaceFolder}/node_modules/mocha/bin/mocha",
"args": [
"--timeout", "999999",
"--colors",
"${workspaceFolder}/test/**/*.test.js"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
使用 console.log 调试 #
javascript
describe('Debug', function() {
it('should debug', function() {
const data = { name: 'John', age: 30 };
console.log('Data:', data);
console.log('JSON:', JSON.stringify(data, null, 2));
expect(data.name).to.equal('John');
});
});
使用 --verbose 输出 #
bash
mocha --verbose test/
测试隔离 #
使用 --file 选项 #
javascript
// .mocharc.js
module.exports = {
file: [
'./test/setup.js',
'./test/helpers.js'
]
};
javascript
// test/setup.js
// 在所有测试之前执行
process.env.NODE_ENV = 'test';
before(function() {
console.log('Global setup');
});
after(function() {
console.log('Global teardown');
});
测试隔离最佳实践 #
javascript
describe('Isolation', function() {
let sandbox;
beforeEach(function() {
// 创建 Sinon sandbox
sandbox = sinon.createSandbox();
});
afterEach(function() {
// 恢复所有 stub 和 spy
sandbox.restore();
});
it('should be isolated', function() {
const stub = sandbox.stub(obj, 'method');
// 测试代码
});
it('should not affect other tests', function() {
// obj.method 已恢复
});
});
动态生成测试 #
使用循环生成测试 #
javascript
describe('Dynamic Tests', function() {
const testCases = [
{ input: 1, expected: 2 },
{ input: 2, expected: 4 },
{ input: 3, expected: 6 },
{ input: 10, expected: 20 }
];
testCases.forEach(({ input, expected }) => {
it(`should double ${input} to ${expected}`, function() {
expect(double(input)).to.equal(expected);
});
});
});
使用 describe.each #
javascript
// 自定义 describe.each 实现
function describeEach(title, testCases, fn) {
describe(title, function() {
testCases.forEach((testCase, index) => {
const caseTitle = typeof testCase === 'object'
? JSON.stringify(testCase)
: String(testCase);
it(`case ${index + 1}: ${caseTitle}`, function() {
fn(testCase);
});
});
});
}
// 使用
describeEach('Math operations', [
{ a: 1, b: 2, sum: 3 },
{ a: 5, b: 3, sum: 8 },
{ a: -1, b: 1, sum: 0 }
], function({ a, b, sum }) {
expect(add(a, b)).to.equal(sum);
});
测试性能优化 #
并行测试 #
javascript
// .mocharc.js
module.exports = {
parallel: true,
jobs: 4
};
增量测试 #
bash
# 只运行修改的测试
mocha --watch
# 使用 git diff 确定测试文件
git diff --name-only HEAD~1 | grep test | xargs mocha
测试分组 #
json
// package.json
{
"scripts": {
"test:unit": "mocha test/unit --timeout 2000",
"test:integration": "mocha test/integration --timeout 10000",
"test:e2e": "mocha test/e2e --timeout 30000"
}
}
测试报告增强 #
多报告器 #
javascript
// mocha-multi-reporters
// npm install mocha-multi-reporters --save-dev
// .mocharc.js
module.exports = {
reporter: 'mocha-multi-reporters',
reporterOptions: {
reporterEnabled: 'spec, xunit',
xunitReporterOptions: {
output: 'test-results.xml'
}
}
};
测试结果输出 #
javascript
// test/hooks.js
afterEach(function() {
if (this.currentTest.state === 'failed') {
console.log('\nFailed test:', this.currentTest.fullTitle());
console.log('Error:', this.currentTest.err.message);
console.log('Stack:', this.currentTest.err.stack);
}
});
测试数据管理 #
使用 Fixtures #
javascript
// test/fixtures/users.js
module.exports = {
validUser: {
name: 'John Doe',
email: 'john@example.com',
age: 30
},
invalidUser: {
name: '',
email: 'invalid-email',
age: -1
},
adminUser: {
name: 'Admin',
email: 'admin@example.com',
role: 'admin'
}
};
javascript
// test/user.test.js
const fixtures = require('./fixtures/users');
describe('User', function() {
it('should create valid user', function() {
const user = new User(fixtures.validUser);
expect(user.isValid()).to.be.true;
});
it('should reject invalid user', function() {
const user = new User(fixtures.invalidUser);
expect(user.isValid()).to.be.false;
});
});
使用 Factory #
javascript
// test/factories/user.js
class UserFactory {
static create(overrides = {}) {
return {
id: Math.random().toString(36).substr(2, 9),
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(),
...overrides
};
}
static createMany(count, overrides = {}) {
return Array.from({ length: count }, (_, i) =>
this.create({ ...overrides, name: `User ${i + 1}` })
);
}
}
module.exports = UserFactory;
javascript
// test/user.test.js
const UserFactory = require('./factories/user');
describe('User', function() {
it('should create user', function() {
const userData = UserFactory.create({ name: 'John' });
expect(userData.name).to.equal('John');
});
it('should create many users', function() {
const users = UserFactory.createMany(5);
expect(users).to.have.lengthOf(5);
});
});
测试最佳实践 #
1. 测试命名 #
javascript
// ✅ 好的命名
describe('UserService', function() {
describe('createUser', function() {
context('when input is valid', function() {
it('should create a new user with correct data', function() {
// ...
});
});
context('when email is invalid', function() {
it('should throw ValidationError', function() {
// ...
});
});
});
});
// ❌ 不好的命名
describe('test', function() {
it('test1', function() {});
});
2. 测试结构 #
javascript
describe('Feature', function() {
// 1. 设置和清理
let subject;
let dependency;
beforeEach(function() {
dependency = sinon.stub();
subject = new Feature(dependency);
});
afterEach(function() {
sinon.restore();
});
// 2. 测试分组
describe('method', function() {
context('scenario', function() {
it('should behave', function() {
// Arrange
dependency.returns('mocked');
// Act
const result = subject.method();
// Assert
expect(result).to.equal('expected');
});
});
});
});
3. 测试覆盖 #
javascript
describe('Calculator', function() {
describe('divide', function() {
// 正常情况
it('should divide two numbers', function() {
expect(divide(6, 2)).to.equal(3);
});
// 边界情况
it('should handle division by 1', function() {
expect(divide(5, 1)).to.equal(5);
});
it('should handle negative numbers', function() {
expect(divide(-6, 2)).to.equal(-3);
});
it('should handle decimal results', function() {
expect(divide(5, 2)).to.equal(2.5);
});
// 错误情况
it('should throw error when dividing by zero', function() {
expect(() => divide(1, 0)).to.throw('Division by zero');
});
});
});
4. 测试隔离 #
javascript
// ✅ 好的做法:每个测试独立
describe('Counter', function() {
let counter;
beforeEach(function() {
counter = new Counter(0);
});
it('should increment', function() {
counter.increment();
expect(counter.value).to.equal(1);
});
it('should start from 0', function() {
expect(counter.value).to.equal(0);
});
});
// ❌ 不好的做法:测试之间有依赖
describe('Counter', function() {
let counter = new Counter(0);
it('should increment', function() {
counter.increment();
expect(counter.value).to.equal(1);
});
it('depends on previous test', function() {
// 这个测试依赖上一个测试
expect(counter.value).to.equal(1);
});
});
常见问题解决 #
1. 测试超时 #
javascript
// 增加超时时间
describe('Slow Tests', function() {
this.timeout(10000);
it('should complete', function(done) {
slowOperation(done);
});
});
2. 未处理的 Promise 拒绝 #
javascript
// 确保所有 Promise 都被处理
it('should handle promise', function() {
return promiseOperation()
.then(result => {
expect(result).to.exist;
});
});
// 或使用 async/await
it('should handle promise', async function() {
const result = await promiseOperation();
expect(result).to.exist;
});
3. 内存泄漏 #
javascript
describe('Memory Management', function() {
let interval;
afterEach(function() {
// 清理定时器
if (interval) {
clearInterval(interval);
interval = null;
}
});
it('should not leak memory', function() {
interval = setInterval(() => {}, 1000);
// 测试代码
});
});
总结 #
恭喜你完成了 Mocha 测试框架的学习!你已经掌握了:
- 基础测试:describe、it、测试组织
- 断言库:Chai 的 expect、should、assert 风格
- 生命周期钩子:before、after、beforeEach、afterEach
- 异步测试:回调、Promise、async/await
- Mock 和 Stub:使用 Sinon 模拟依赖
- 配置优化:配置文件、报告器、并行测试
- 高级特性:浏览器测试、调试、最佳实践
继续实践,编写高质量的测试代码!
最后更新:2026-03-28