从TDD走到ATDD:测试驱动开发笔记
测试驱动开发笔记相关的坑,多半出在边界条件上。
这次做测试驱动开发,从 TDD 到 ATDD,。
为什么尝试 TDD
// 以前的开发方式:先写代码,再写测试
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
// 然后写测试
test('calculateTotal should sum prices', () => {
const items = [
{ price: 10 },
{ price: 20 },
{ price: 30 }
];
expect(calculateTotal(items)).toBe(60);
});
问题:测试是补上的,覆盖率低,设计缺陷不容易发现。
TDD 基础流程
// Red-Green-Refactor
// 1. Red:写一个失败的测试
test('calculateTotal with empty array should return 0', () => {
expect(calculateTotal([])).toBe(0);
});
// 运行测试,失败(函数还没写)
// 2. Green:让测试通过
function calculateTotal(items) {
if (items.length === 0) return 0;
return items.reduce((sum, item) => sum + item.price, 0);
}
// 运行测试,通过
// 3. Refactor:重构代码,保持测试通过
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
实际案例
// 场景:用户注册功能
// Red 1
test('should create user with valid data', () => {
const userData = {
name: 'Alice',
email: '[email protected]',
password: 'secure123'
};
const user = userService.register(userData);
expect(user.id).toBeDefined();
expect(user.name).toBe(userData.name);
});
// Green 1
class UserService {
register(userData) {
const user = {
id: Date.now(),
...userData
};
return user;
}
}
// Red 2:验证邮箱格式
test('should throw error for invalid email', () => {
const userData = {
name: 'Alice',
email: 'invalid-email',
password: 'secure123'
};
expect(() => userService.register(userData)).toThrow('Invalid email');
});
// Green 2
class UserService {
register(userData) {
this.validateEmail(userData.email);
this.validatePassword(userData.password);
const user = {
id: Date.now(),
...userData
};
return user;
}
validateEmail(email) {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
throw new Error('Invalid email');
}
}
validatePassword(password) {
if (password.length < 8) {
throw new Error('Password too short');
}
}
}
ATDD 实践
// Acceptance Test-Driven Development
// 1. 定义验收标准
// Feature: 用户注册
// 用户应该能够使用有效的邮箱和密码注册账户
// 用户不应该能够使用无效的邮箱注册账户
// 密码应该至少 8 个字符
// 2. 编写验收测试
const { Given, When, Then } = require('cucumber');
Given('a user with valid registration data', function () {
this.userData = {
name: 'Alice',
email: '[email protected]',
password: 'secure12345'
};
});
When('the user attempts to register', function () {
this.result = userService.register(this.userData);
});
Then('the user should be created successfully', function () {
expect(this.result.id).toBeDefined();
});
Given('a user with invalid email', function () {
this.userData = {
name: 'Alice',
email: 'invalid-email',
password: 'secure12345'
};
});
Then('registration should fail', function () {
expect(() => userService.register(this.userData)).toThrow();
});
踩过的坑
坑一:测试质量差
为了追求覆盖率写了大量无意义测试。
解决:关注有意义的测试,质量优先于数量。
// 不好的测试
test('true should be true', () => {
expect(true).toBe(true);
});
// 好的测试
test('should handle empty cart checkout', () => {
const cart = createEmptyCart();
expect(() => checkout(cart)).toThrow('Cart is empty');
});
坑二:测试成为负担
测试代码比业务代码还多,维护困难。
解决:简化测试,只测试关键路径。
// 简化测试
test('should calculate order total correctly', () => {
const order = createOrder([
{ name: 'Item 1', price: 10, quantity: 2 }
]);
expect(order.getTotal()).toBe(20);
});
坑三:TDD 流程不顺畅
团队不习惯 TDD,流程不顺畅。
解决:循序渐进,先在关键模块实践。
// 从小范围开始
// 1. 在新功能中尝试 TDD
// 2. 在重构中尝试 TDD
// 3. 逐步扩展到整个项目
写在最后
TDD 这东西,不只是测试,是设计方法论。
解决了:
- Bug 减少
- 代码质量提升
- 重构信心增强
带来了:
- 开发时间增加
- 学习成本
- 团队接受度
实践之前先评估:
- 项目类型
- 团队接受度
- 测试文化
- 时间预算
不是所有项目都需要 TDD,有时候单元测试就够用。
这次 TDD 实践花了一个月,从 TDD 到 ATDD。实践完成后,Bug 率降低了 60%,代码重构信心大幅提升。
可用性说明:本文发布于 2021 年 1 月,距今已超过五年。文中涉及的软件版本、接口、下载地址、命令参数和操作界面可能已经发生变化,部分方案在当前环境下可能失效。请结合官方最新文档核对后再操作,生产环境使用前务必先行验证。
版权声明: 本文首发于 指尖魔法屋-从TDD走到ATDD:测试驱动开发笔记(https://blog.thinkmoon.cn/post/103-tdd-atdd-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。