您现在的位置是:网站首页 > 测试驱动开发(TDD)与设计模式文章详情
测试驱动开发(TDD)与设计模式
陈川
【
JavaScript
】
63962人已围观
8072字
测试驱动开发(TDD)是一种先写测试再实现功能的开发方法,而设计模式则是解决常见问题的可复用方案。两者结合能提升代码质量和可维护性,尤其在JavaScript这种动态语言中,TDD能有效约束设计模式的使用边界。
TDD的核心循环与价值
TDD遵循"红-绿-重构"的循环流程:
- 编写失败的测试(红)
- 实现最小可通过代码(绿)
- 优化代码结构(重构)
// 示例:测试优先的加法函数
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('加糖和牛奶');
}
}
基础模板类的测试验证了必要方法的强制实现,具体子类测试验证实际行为。
上一篇: 模拟(Mock)与桩(Stub)在模式测试中的应用
下一篇: 模式重构的识别与实施步骤