Testing strategy for pattern combinations
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:
- Verifying the observer notification mechanism
- Checking if the strategy is correctly injected
- 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
- 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);
});
- 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');
});
- 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
上一篇:重构工具与设计模式转换
下一篇:行为保持的重构方法