阿里云主机折上折
  • 微信号
Current Site:Index > Testing strategy for pattern combinations

Testing strategy for pattern combinations

Author:Chuan Chen 阅读数:50645人阅读 分类: JavaScript

Testing Strategies for Pattern Combinations

Design patterns are often combined in JavaScript development to solve complex problems, but testing combined patterns is typically more challenging than testing individual patterns. Testing pattern combinations requires focusing on the interaction boundaries between patterns, their dependencies, and whether the overall behavior meets expectations.

Core Challenges in Testing Pattern Combinations

When multiple design patterns are combined, testing complexity grows exponentially. For example, when combining the Observer pattern with the Strategy pattern, it's necessary to verify whether state changes trigger the correct strategy execution:

// Observer + Strategy combination example
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('Execute Strategy A')
};

const subject = new Subject();
subject.observers.push(new ConcreteObserver());
subject.setStrategy(strategyA); // Should trigger strategy execution

Testing this combination requires:

  1. Verifying the observer notification mechanism
  2. Checking if the strategy is correctly injected
  3. Confirming the timing of strategy execution

Layered Testing Strategy

Unit Testing Layer

Validate each pattern independently to ensure basic functionality works. Below is a test for combining the Factory Method with the Singleton pattern:

// Factory + Singleton combination
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(); // Returns a singleton
}

// Test case
describe('Logger Combination', () => {
  it('should return the same singleton instance', () => {
    const logger1 = createLogger();
    const logger2 = createLogger();
    expect(logger1).toBe(logger2);
  });
});

Integration Testing Layer

Validate interactions between patterns, such as combining the Decorator pattern with the Adapter pattern:

// Decorator + Adapter combination
class OldService {
  request() { return "Old data format"; }
}

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('Request started');
    const result = original.apply(this);
    console.log('Request ended');
    return result;
  };
  return service;
}

// Integration test
test('Decorator should log adapter calls', () => {
  const mockConsole = jest.spyOn(console, 'log');
  const adapted = new Adapter(new OldService());
  const decorated = loggingDecorator(adapted);
  
  decorated.newRequest();
  
  expect(mockConsole).toHaveBeenCalledWith('Request started');
  expect(mockConsole).toHaveBeenCalledWith('Request ended');
});

Contract Testing Strategy

When pattern combinations involve multiple modules, it's essential to define interaction contracts between them. Example of combining the Mediator pattern with the Command pattern:

// Mediator + Command combination
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(`Unregistered command: ${commandName}`);
  }
}

class CreateUserCommand {
  execute(payload) {
    return `Create user: ${payload.name}`;
  }
}

// Contract test
describe('Mediator-Command Contract', () => {
  let mediator;
  
  beforeEach(() => {
    mediator = new Mediator();
    mediator.register('createUser', new CreateUserCommand());
  });

  it('should reject unregistered commands', () => {
    expect(() => mediator.execute('unknown')).toThrow();
  });

  it('should return command execution results', () => {
    const result = mediator.execute('createUser', { name: 'Test User' });
    expect(result).toMatch(/Create user/);
  });
});

Visual Testing Techniques

For complex pattern combinations, snapshot testing can be used to record component states. Example for a React component combining State and Strategy patterns:

// State + Strategy combination component
class PaymentState {
  constructor(strategy) {
    this.strategy = strategy;
  }

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

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

// Test case
test('Payment strategy rendering snapshot', () => {
  const state = new PaymentState(CreditCardStrategy);
  const wrapper = render(state.render());
  expect(wrapper.container).toMatchSnapshot();
});

Testing Pyramid Practice

  1. Basic Unit Tests: Cover independent functionality of each pattern
// Singleton pattern basic test
test('Singleton instance uniqueness', () => {
  const instance1 = new Singleton();
  const instance2 = new Singleton();
  expect(instance1).toBe(instance2);
});
  1. Interaction Tests: Validate behavior after pattern combination
// Observer + Factory combination test
test('Factory-created observers should receive notifications', () => {
  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. End-to-End Tests: Validate complete workflows
// State Machine + Strategy + Command combination test
test('Complete order process', 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);
});

Test Data Construction Techniques

Use the Builder pattern to simplify creating complex test data:

// Test data builder
class UserBuilder {
  constructor() {
    this.user = {
      name: 'Default User',
      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;
  }
}

// Usage in tests
test('Should allow admin access', () => {
  const adminUser = new UserBuilder()
    .withAdminRole()
    .withPreference('theme', 'dark')
    .build();
    
  const result = checkAccess(adminUser);
  expect(result).toBeTruthy();
});

Asynchronous Pattern Combination Testing

Special handling is required when pattern combinations involve asynchronous operations. Example test for combining Pub/Sub with Promises:

// Async Pub/Sub implementation
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;
  }
}

// Async test
describe('AsyncPubSub', () => {
  it('should process async subscribers sequentially', async () => {
    const pubsub = new AsyncPubSub();
    const mockSubscriber1 = jest.fn(() => 
      new Promise(res => setTimeout(() => res('Result 1'), 10))
    );
    const mockSubscriber2 = jest.fn(() => Promise.resolve('Result 2'));
    
    pubsub.subscribe(mockSubscriber1);
    pubsub.subscribe(mockSubscriber2);
    
    const results = await pubsub.publish('Test data');
    
    expect(results).toEqual(['Result 1', 'Result 2']);
    expect(mockSubscriber1).toHaveBeenCalledBefore(mockSubscriber2);
  });
});

Test Coverage Optimization

Add boundary tests for special cases in pattern combinations:

// Proxy + Cache combination boundary tests
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;
  }
}

// Boundary test cases
describe('CacheProxy Edge Cases', () => {
  it('should handle concurrent duplicate requests', async () => {
    const mockService = {
      fetch: jest.fn(() => 
        new Promise(res => setTimeout(() => res('Data'), 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('should handle null cache values', async () => {
    const proxy = new CacheProxy({ fetch: () => null });
    const result = await proxy.getData('nullKey');
    expect(result).toBeNull();
  });
});

Performance Testing for Pattern Combinations

Some pattern combinations may introduce performance issues that require specific testing:

// Decorator chain performance test
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('Random validation failed');
      return original.apply(this);
    };
    return service;
  }

  // Test performance impact of decorator stacking
  test('Decorator stacking performance cost', () => {
    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

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.