阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 模式组合的测试策略

模式组合的测试策略

作者:陈川 阅读数:50643人阅读 分类: JavaScript

模式组合的测试策略

设计模式在JavaScript开发中常被组合使用以解决复杂问题,但组合后的测试策略往往比单一模式更复杂。测试组合模式需要关注模式间的交互边界、依赖关系以及整体行为是否符合预期。

组合模式测试的核心挑战

当多个设计模式组合使用时,测试复杂度呈指数级增长。观察者模式与策略模式组合时,需要验证状态变化是否触发正确的策略执行:

// 观察者 + 策略组合示例
class Subject {
  constructor() {
    this.observers = [];
    this.currentStrategy = null;
  }

  setStrategy(strategy) {
    this.currentStrategy = strategy;
    this.notify();
  }

  notify() {
    this.observers.forEach(observer => 
      observer.update(this.currentStrategy)
    );
  }
}

class ConcreteObserver {
  update(strategy) {
    strategy.execute();
  }
}

const strategyA = {
  execute: () => console.log('执行策略A')
};

const subject = new Subject();
subject.observers.push(new ConcreteObserver());
subject.setStrategy(strategyA); // 应触发策略执行

测试这种组合需要:

  1. 验证观察者通知机制
  2. 检查策略是否正确注入
  3. 确认策略执行时机

分层测试策略

单元测试层

对每个模式进行独立验证,确保基础功能正常。以下测试工厂方法与单例模式的组合:

// 工厂 + 单例组合
class Logger {
  constructor() {
    if (Logger.instance) return Logger.instance;
    this.logs = [];
    Logger.instance = this;
  }
}

function createLogger(type) {
  if (type === 'file') return new FileLogger();
  return new Logger(); // 返回单例
}

// 测试用例
describe('Logger组合', () => {
  it('应返回相同单例实例', () => {
    const logger1 = createLogger();
    const logger2 = createLogger();
    expect(logger1).toBe(logger2);
  });
});

集成测试层

验证模式间的交互,例如装饰器模式与适配器模式的组合:

// 装饰器 + 适配器组合
class OldService {
  request() { return "旧数据格式"; }
}

class Adapter {
  constructor(oldService) {
    this.oldService = oldService;
  }

  newRequest() {
    const data = this.oldService.request();
    return { formatted: true, data };
  }
}

function loggingDecorator(service) {
  const original = service.newRequest;
  service.newRequest = function() {
    console.log('请求开始');
    const result = original.apply(this);
    console.log('请求结束');
    return result;
  };
  return service;
}

// 集成测试
test('装饰器应记录适配器调用', () => {
  const mockConsole = jest.spyOn(console, 'log');
  const adapted = new Adapter(new OldService());
  const decorated = loggingDecorator(adapted);
  
  decorated.newRequest();
  
  expect(mockConsole).toHaveBeenCalledWith('请求开始');
  expect(mockConsole).toHaveBeenCalledWith('请求结束');
});

契约测试策略

当组合模式涉及多个模块时,需要明确模块间的交互契约。中介者模式与命令模式的组合示例:

// 中介者 + 命令组合
class Mediator {
  constructor() {
    this.commands = {};
  }

  register(commandName, command) {
    this.commands[commandName] = command;
  }

  execute(commandName, payload) {
    if (this.commands[commandName]) {
      return this.commands[commandName].execute(payload);
    }
    throw new Error(`未注册命令: ${commandName}`);
  }
}

class CreateUserCommand {
  execute(payload) {
    return `创建用户: ${payload.name}`;
  }
}

// 契约测试
describe('中介者命令契约', () => {
  let mediator;
  
  beforeEach(() => {
    mediator = new Mediator();
    mediator.register('createUser', new CreateUserCommand());
  });

  it('应拒绝未注册命令', () => {
    expect(() => mediator.execute('unknown')).toThrow();
  });

  it('应返回命令执行结果', () => {
    const result = mediator.execute('createUser', { name: '测试用户' });
    expect(result).toMatch(/创建用户/);
  });
});

可视化测试技术

对于复杂的模式组合,可以采用快照测试记录组件状态。以下是用在状态模式与策略模式组合的React组件示例:

// 状态 + 策略组合组件
class PaymentState {
  constructor(strategy) {
    this.strategy = strategy;
  }

  render() {
    return this.strategy.render();
  }
}

const CreditCardStrategy = {
  render: () => (
    <div className="credit-card">
      <input type="text" placeholder="卡号" />
    </div>
  )
};

// 测试用例
test('支付策略渲染快照', () => {
  const state = new PaymentState(CreditCardStrategy);
  const wrapper = render(state.render());
  expect(wrapper.container).toMatchSnapshot();
});

测试金字塔实践

  1. 基础单元测试:覆盖每个模式的独立功能
// 单例模式基础测试
test('单例实例唯一性', () => {
  const instance1 = new Singleton();
  const instance2 = new Singleton();
  expect(instance1).toBe(instance2);
});
  1. 交互测试:验证模式组合后的行为
// 观察者+工厂组合测试
test('工厂创建的观察者应接收通知', () => {
  const observer = createObserver('email');
  const subject = new Subject();
  subject.subscribe(observer);
  
  const mockHandler = jest.fn();
  observer.handleUpdate = mockHandler;
  
  subject.notify('test');
  expect(mockHandler).toHaveBeenCalledWith('test');
});
  1. 端到端测试:验证完整工作流
// 状态机+策略+命令组合测试
test('完整订单流程', async () => {
  const order = new Order();
  order.setState(new NewState());
  order.applyStrategy(new DiscountStrategy());
  
  await order.process(new ProcessCommand());
  
  expect(order.getState()).toBeInstanceOf(ProcessedState);
  expect(order.getTotal()).toBeLessThan(originalTotal);
});

测试数据构造技巧

构建测试数据时可采用建造者模式简化复杂对象的创建:

// 测试数据建造者
class UserBuilder {
  constructor() {
    this.user = {
      name: '默认用户',
      roles: ['guest'],
      preferences: {}
    };
  }

  withAdminRole() {
    this.user.roles.push('admin');
    return this;
  }

  withPreference(key, value) {
    this.user.preferences[key] = value;
    return this;
  }

  build() {
    return this.user;
  }
}

// 在测试中使用
test('应允许管理员访问', () => {
  const adminUser = new UserBuilder()
    .withAdminRole()
    .withPreference('theme', 'dark')
    .build();
    
  const result = checkAccess(adminUser);
  expect(result).toBeTruthy();
});

异步组合模式测试

当组合模式涉及异步操作时,需要特殊处理。以下是发布/订阅模式与Promise的组合测试:

// 异步发布/订阅实现
class AsyncPubSub {
  constructor() {
    this.subscribers = [];
  }

  subscribe(fn) {
    this.subscribers.push(fn);
  }

  async publish(data) {
    const results = [];
    for (const subscriber of this.subscribers) {
      results.push(await subscriber(data));
    }
    return results;
  }
}

// 异步测试
describe('AsyncPubSub', () => {
  it('应顺序处理异步订阅者', async () => {
    const pubsub = new AsyncPubSub();
    const mockSubscriber1 = jest.fn(() => 
      new Promise(res => setTimeout(() => res('结果1'), 10))
    );
    const mockSubscriber2 = jest.fn(() => Promise.resolve('结果2'));
    
    pubsub.subscribe(mockSubscriber1);
    pubsub.subscribe(mockSubscriber2);
    
    const results = await pubsub.publish('测试数据');
    
    expect(results).toEqual(['结果1', '结果2']);
    expect(mockSubscriber1).toHaveBeenCalledBefore(mockSubscriber2);
  });
});

测试覆盖率优化

针对模式组合的特殊情况,需要增加边界测试:

// 代理模式 + 缓存组合的边界测试
class CacheProxy {
  constructor(service) {
    this.service = service;
    this.cache = new Map();
  }

  async getData(key) {
    if (this.cache.has(key)) {
      return this.cache.get(key);
    }
    const data = await this.service.fetch(key);
    this.cache.set(key, data);
    return data;
  }
}

// 边界测试用例
describe('CacheProxy边界情况', () => {
  it('应处理并发重复请求', async () => {
    const mockService = {
      fetch: jest.fn(() => 
        new Promise(res => setTimeout(() => res('数据'), 100))
    };
    const proxy = new CacheProxy(mockService);
    
    const [res1, res2] = await Promise.all([
      proxy.getData('key'),
      proxy.getData('key')
    ]);
    
    expect(res1).toBe(res2);
    expect(mockService.fetch).toHaveBeenCalledTimes(1);
  });

  it('应处理null缓存值', async () => {
    const proxy = new CacheProxy({ fetch: () => null });
    const result = await proxy.getData('nullKey');
    expect(result).toBeNull();
  });
});

模式组合的性能测试

某些模式组合可能引入性能问题,需要专门测试:

// 装饰器链性能测试
function benchmark() {
  class CoreService {
    heavyOperation() {
      let total = 0;
      for (let i = 0; i < 1e6; i++) {
        total += Math.random();
      }
      return total;
    }
  }

  function loggingDecorator(service) {
    const original = service.heavyOperation;
    service.heavyOperation = function() {
      console.time('operation');
      const result = original.apply(this);
      console.timeEnd('operation');
      return result;
    };
    return service;
  }

  function validationDecorator(service) {
    const original = service.heavyOperation;
    service.heavyOperation = function() {
      if (Math.random() > 0.5) throw new Error('随机验证失败');
      return original.apply(this);
    };
    return service;
  }

  // 测试装饰器叠加性能影响
  test('装饰器叠加性能损耗', () => {
    const raw = new CoreService();
    const decorated = validationDecorator(loggingDecorator(new CoreService()));
    
    const rawStart = performance.now();
    raw.heavyOperation();
    const rawDuration = performance.now() - rawStart;
    
    const decoratedStart = performance.now();
    try { decorated.heavyOperation(); } catch {}
    const decoratedDuration = performance.now() - decoratedStart;
    
    expect(decoratedDuration).toBeLessThan(rawDuration * 1.5);
  });
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益,请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌