从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/) 转载或引用必须申明原指尖魔法屋来源及源地址!