您现在的位置是:网站首页 > 测试驱动开发(TDD)与设计模式文章详情

测试驱动开发(TDD)与设计模式

测试驱动开发(TDD)是一种先写测试再实现功能的开发方法,而设计模式则是解决常见问题的可复用方案。两者结合能提升代码质量和可维护性,尤其在JavaScript这种动态语言中,TDD能有效约束设计模式的使用边界。

TDD的核心循环与价值

TDD遵循"红-绿-重构"的循环流程:

  1. 编写失败的测试(红)
  2. 实现最小可通过代码(绿)
  3. 优化代码结构(重构)
// 示例:测试优先的加法函数
describe('add function', () => {
  it('should return 3 when adding 1 and 2', () => {
    assert.equal(add(1, 2), 3);
  });
});

// 初始实现
function add(a, b) {
  return a + b;
}

这种实践强制开发者思考接口设计而非实现细节。当结合设计模式时,测试成为模式的"第一个客户",能验证模式应用的合理性。

工厂模式与TDD实践

工厂模式通过统一接口创建对象,TDD可以帮助确定工厂的边界。假设我们要创建不同形状的绘图工具:

// 测试用例
describe('ShapeFactory', () => {
  it('should create circle with correct radius', () => {
    const circle = ShapeFactory.create('circle', { radius: 5 });
    assert.equal(circle.area(), Math.PI * 25);
  });

  it('should throw error for unknown shape', () => {
    assert.throws(() => ShapeFactory.create('hexagon'));
  });
});

// 实现
class ShapeFactory {
  static create(type, params) {
    switch(type) {
      case 'circle':
        return new Circle(params.radius);
      case 'rectangle':
        return new Rectangle(params.width, params.height);
      default:
        throw new Error(`Unknown shape type: ${type}`);
    }
  }
}

TDD确保工厂的扩展不会破坏现有功能,每次新增形状类型时都需要先编写测试。

观察者模式的事件系统测试

观察者模式实现松耦合的事件处理,TDD能验证事件订阅和发布的正确性:

describe('EventEmitter', () => {
  let emitter;
  beforeEach(() => emitter = new EventEmitter());

  it('should call subscriber when event occurs', () => {
    const spy = sinon.spy();
    emitter.on('click', spy);
    emitter.emit('click');
    assert(spy.calledOnce);
  });

  it('should pass event data to subscribers', () => {
    const testData = { x: 10, y: 20 };
    emitter.on('click', data => {
      assert.deepEqual(data, testData);
    });
    emitter.emit('click', testData);
  });
});

// 最小实现
class EventEmitter {
  constructor() {
    this.events = {};
  }

  on(event, listener) {
    (this.events[event] || (this.events[event] = [])).push(listener);
  }

  emit(event, ...args) {
    (this.events[event] || []).forEach(listener => listener(...args));
  }
}

测试驱动确保事件系统在增加新功能(如取消订阅)时保持行为一致。

策略模式与测试隔离

策略模式将算法封装为可互换的对象,TDD可以独立验证每个策略:

describe('PricingStrategies', () => {
  describe('RegularPrice', () => {
    it('should return base price', () => {
      const strategy = new RegularPrice();
      assert.equal(strategy.calculate(100), 100);
    });
  });

  describe('DiscountPrice', () => {
    it('should apply 20% discount', () => {
      const strategy = new DiscountPrice(20);
      assert.equal(strategy.calculate(100), 80);
    });
  });

  describe('PriceCalculator', () => {
    it('should use current strategy', () => {
      const calculator = new PriceCalculator(new RegularPrice());
      assert.equal(calculator.execute(100), 100);
      
      calculator.setStrategy(new DiscountPrice(10));
      assert.equal(calculator.execute(100), 90);
    });
  });
});

// 策略实现
class RegularPrice {
  calculate(price) { return price; }
}

class DiscountPrice {
  constructor(percent) {
    this.percent = percent;
  }
  
  calculate(price) {
    return price * (1 - this.percent / 100);
  }
}

class PriceCalculator {
  constructor(strategy) {
    this.strategy = strategy;
  }
  
  setStrategy(strategy) {
    this.strategy = strategy;
  }
  
  execute(price) {
    return this.strategy.calculate(price);
  }
}

每个策略的测试完全独立,符合TDD的单一职责原则。

装饰器模式的增量测试

装饰器模式动态添加职责,TDD适合验证层层装饰的效果:

describe('TextDecorators', () => {
  const simpleText = new PlainText('Hello');
  
  it('should wrap with bold tags', () => {
    const boldText = new BoldDecorator(simpleText);
    assert.equal(boldText.render(), '<b>Hello</b>');
  });

  it('should combine multiple decorators', () => {
    const decorated = new ItalicDecorator(
      new BoldDecorator(simpleText)
    );
    assert.equal(decorated.render(), '<i><b>Hello</b></i>');
  });
});

// 基础组件
class PlainText {
  constructor(content) {
    this.content = content;
  }
  
  render() {
    return this.content;
  }
}

// 装饰器基类
class TextDecorator {
  constructor(textComponent) {
    this.component = textComponent;
  }
  
  render() {
    return this.component.render();
  }
}

// 具体装饰器
class BoldDecorator extends TextDecorator {
  render() {
    return `<b>${super.render()}</b>`;
  }
}

class ItalicDecorator extends TextDecorator {
  render() {
    return `<i>${super.render()}</i>`;
  }
}

TDD确保每个装饰器单独工作且组合后行为正确,防止装饰顺序导致的意外行为。

TDD对设计模式选择的反馈

当测试难以编写时,可能预示着模式选择不当。例如,过度使用单例模式会导致测试困难:

// 难以测试的单例示例
class Database {
  static instance;
  
  constructor() {
    if (Database.instance) {
      return Database.instance;
    }
    // 复杂的初始化逻辑
    Database.instance = this;
  }
  
  query() { /* ... */ }
}

// 测试时会遇到问题:
// 1. 无法重置实例状态
// 2. 测试间相互影响

TDD会推动改用依赖注入等更可测试的模式:

class Database {
  constructor(config) {
    // 初始化逻辑
  }
}

// 测试中可以创建独立实例
describe('Database', () => {
  it('should execute queries', () => {
    const db = new Database(testConfig);
    // 测试代码
  });
});

测试替身在模式验证中的应用

测试替身(Mock/Stub)对验证模式行为至关重要。以下验证代理模式:

describe('ImageProxy', () => {
  it('should lazy load real image', () => {
    const mockImage = { load: sinon.spy() };
    const proxy = new ImageProxy(() => mockImage);
    
    proxy.display(); // 不应立即加载
    assert.isFalse(mockImage.load.called);
    
    proxy.click(); // 触发加载
    assert.isTrue(mockImage.load.called);
  });
});

class ImageProxy {
  constructor(imageFactory) {
    this.imageFactory = imageFactory;
    this.realImage = null;
  }
  
  display() {
    console.log('显示占位图');
  }
  
  click() {
    this.realImage = this.realImage || this.imageFactory();
    this.realImage.load();
  }
}

通过注入伪造的真实图片,可以精确测试代理的延迟加载行为。

组合模式与递归测试

组合模式处理树形结构,TDD需要特殊考虑递归测试:

describe('FileSystem', () => {
  const root = new Directory('root');
  const docs = new Directory('docs');
  const file = new File('readme.txt');
  
  beforeEach(() => {
    root.add(docs);
    docs.add(file);
  });

  it('should calculate total size', () => {
    file.size = 100;
    assert.equal(root.totalSize(), 100);
  });

  it('should find file by name', () => {
    assert.equal(root.find('readme.txt'), file);
    assert.isNull(root.find('missing.txt'));
  });
});

class FileSystemItem {
  constructor(name) {
    this.name = name;
  }
}

class File extends FileSystemItem {
  size = 0;
  totalSize() { return this.size; }
  find(name) { return this.name === name ? this : null; }
}

class Directory extends FileSystemItem {
  children = [];
  
  add(item) {
    this.children.push(item);
  }
  
  totalSize() {
    return this.children.reduce((sum, child) => sum + child.totalSize(), 0);
  }
  
  find(name) {
    for (const child of this.children) {
      const found = child.find(name);
      if (found) return found;
    }
    return null;
  }
}

测试验证了组合模式的递归行为,确保子节点增减不影响整体结构。

模板方法模式的测试策略

模板方法定义算法骨架,TDD需要分别测试抽象步骤和具体实现:

describe('BeverageTemplate', () => {
  describe('abstract methods', () => {
    it('should throw error if not implemented', () => {
      const beverage = new BeverageTemplate();
      assert.throws(() => beverage.brew());
      assert.throws(() => beverage.addCondiments());
    });
  });

  describe('Coffee implementation', () => {
    it('should execute template method', () => {
      const coffee = new Coffee();
      const spy = sinon.spy(coffee, 'addCondiments');
      
      coffee.prepare();
      assert(spy.calledOnce);
      assert.equal(coffee.brew(), '冲泡咖啡粉');
    });
  });
});

class BeverageTemplate {
  prepare() {
    this.boilWater();
    this.brew();
    this.pourInCup();
    this.addCondiments();
  }
  
  boilWater() {
    console.log('煮沸水');
  }
  
  pourInCup() {
    console.log('倒入杯子');
  }
  
  brew() {
    throw new Error('必须实现brew方法');
  }
  
  addCondiments() {
    throw new Error('必须实现addCondiments方法');
  }
}

class Coffee extends BeverageTemplate {
  brew() {
    return '冲泡咖啡粉';
  }
  
  addCondiments() {
    console.log('加糖和牛奶');
  }
}

基础模板类的测试验证了必要方法的强制实现,具体子类测试验证实际行为。

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步