一、为什么需要测试驱动开发
想象一下你正在搭积木。如果每放一块新积木都检查一次整体结构是否稳固,肯定比全部搭完再测试要靠谱得多。测试驱动开发(TDD)就是这个道理——先写测试用例,再写实现代码。这样做有三个明显好处:
- 代码质量更高(就像搭积木时的实时检查)
- 修改代码时更有信心(有测试用例当安全网)
- 设计更合理(写测试时就在思考接口设计)
举个生活中的例子:你要做西红柿炒蛋,TDD相当于先想好"咸淡适中、鸡蛋嫩滑、西红柿不出水"这些标准,再开始烹饪。
二、Mocha和Chai的基本使用
技术栈:Node.js + Mocha + Chai
先安装必要的包:
npm install mocha chai --save-dev
基础测试示例:
// 测试文件:test/add.test.js
const chai = require('chai');
const expect = chai.expect; // 使用expect风格断言
// 要测试的函数
function add(a, b) {
return a + b;
}
describe('加法函数测试', () => { // 测试套件
it('应该正确计算两个正数的和', () => { // 测试用例
expect(add(1, 2)).to.equal(3);
});
it('应该处理负数相加', () => {
expect(add(-1, -2)).to.equal(-3);
});
});
运行测试:
npx mocha test/add.test.js
这个例子展示了最基本的测试结构:
describe定义测试套件(相当于功能模块)it定义单个测试用例expect进行断言验证
三、测试异步代码的几种方式
技术栈:Node.js + Mocha + Chai
异步测试是Node.js的特色,Mocha提供了三种写法:
- 回调函数方式
describe('异步回调测试', () => {
it('应该在1秒后返回结果', (done) => { // 注意done参数
setTimeout(() => {
expect(1 + 1).to.equal(2);
done(); // 告诉Mocha测试完成
}, 1000);
});
});
- Promise方式
describe('Promise测试', () => {
it('应该resolve正确的值', () => {
return new Promise((resolve) => { // 直接返回Promise
setTimeout(() => resolve(42), 500);
}).then(result => {
expect(result).to.equal(42);
});
});
});
- async/await方式(推荐)
describe('async/await测试', () => {
it('应该等待异步操作完成', async () => { // 使用async标记
const result = await new Promise(resolve => {
setTimeout(() => resolve('done'), 300);
});
expect(result).to.equal('done');
});
});
四、测试真实场景的Node.js应用
技术栈:Node.js + Mocha + Chai
让我们测试一个简单的用户服务模块:
// 用户服务模块:services/userService.js
class UserService {
constructor() {
this.users = [];
}
addUser(user) {
if (!user.name) throw new Error('用户名不能为空');
this.users.push(user);
return user;
}
findUserById(id) {
return new Promise(resolve => {
setTimeout(() => { // 模拟数据库查询
resolve(this.users.find(u => u.id === id));
}, 100);
});
}
}
module.exports = UserService;
对应的测试文件:
// 测试文件:test/userService.test.js
const chai = require('chai');
const expect = chai.expect;
const UserService = require('../services/userService');
describe('用户服务测试', () => {
let userService;
beforeEach(() => { // 每个测试用例前执行
userService = new UserService();
});
describe('添加用户', () => {
it('应该成功添加有效用户', () => {
const user = { id: 1, name: '张三' };
const result = userService.addUser(user);
expect(result).to.deep.equal(user); // 深度比较对象
});
it('应该拒绝空用户名', () => {
expect(() => userService.addUser({ id: 2 }))
.to.throw('用户名不能为空');
});
});
describe('查询用户', () => {
it('应该返回正确的用户', async () => {
const user = { id: 3, name: '李四' };
userService.addUser(user);
const result = await userService.findUserById(3);
expect(result).to.deep.equal(user);
});
it('应该返回undefined当用户不存在', async () => {
const result = await userService.findUserById(999);
expect(result).to.be.undefined;
});
});
});
这个例子展示了:
- 如何测试类方法
- 同步和异步测试的结合使用
- 使用
beforeEach进行测试初始化 - 异常情况的测试
五、高级测试技巧和最佳实践
技术栈:Node.js + Mocha + Chai
- 测试替身(Test Doubles)
// 测试文件:test/paymentService.test.js
const chai = require('chai');
const expect = chai.expect;
const sinon = require('sinon'); // 引入sinon库
describe('支付服务测试', () => {
it('应该调用第三方支付接口', () => {
const paymentService = {
pay: () => {} // 空实现
};
// 创建替身(spy)
const spy = sinon.spy(paymentService, 'pay');
// 调用业务逻辑(这里简化了)
paymentService.pay(100);
// 验证是否被调用
expect(spy.calledOnce).to.be.true;
expect(spy.calledWith(100)).to.be.true;
});
});
- 测试覆盖率 安装nyc工具:
npm install nyc --save-dev
运行测试并收集覆盖率:
npx nyc mocha
- 最佳实践:
- 测试名称应该清晰描述预期行为
- 每个测试只验证一个功能点
- 避免测试实现细节,关注输入输出
- 定期检查测试覆盖率(建议至少80%)
六、应用场景与注意事项
适用场景:
- 长期维护的项目(测试能显著降低维护成本)
- 核心业务逻辑(需要确保绝对正确)
- 多人协作开发(测试用例是最好的文档)
技术优缺点:
优点:
- 提前发现设计问题
- 减少调试时间
- 便于重构
- 作为活文档
缺点:
- 初期学习曲线较陡
- 需要额外时间编写测试
- 不适合快速原型开发
注意事项:
- 不要为了覆盖率而写测试
- 避免过度依赖实现细节的测试
- 保持测试代码和生产代码同等质量
- 定期清理过时的测试用例
七、总结
测试驱动开发就像给代码系上安全带,虽然刚开始可能觉得麻烦,但关键时刻能救命。通过Mocha和Chai的组合,我们能够:
- 从简单到复杂逐步构建测试
- 处理各种同步/异步场景
- 应用测试替身等高级技巧
- 建立可靠的测试安全网
记住,好的测试应该像好警察一样——不找麻烦,但能防止麻烦。开始可能不习惯,但坚持几周后,你会发现没有测试的代码就像没系安全带的驾驶,心里总不踏实。
评论