您现在的位置是:网站首页 > 单元测试中的模式隔离技巧文章详情
单元测试中的模式隔离技巧
陈川
【
JavaScript
】
6138人已围观
6864字
单元测试是保证代码质量的重要手段,而模式隔离则是确保测试独立性和可维护性的关键。在JavaScript中,设计模式的应用往往涉及复杂的依赖关系,如何通过隔离技巧避免测试间的相互干扰,是开发者需要重点解决的问题。
依赖注入与测试替身
依赖注入(Dependency Injection)是实现模式隔离的基础。通过将依赖项从实现中解耦,可以轻松替换为测试替身(Test Doubles)。常见的测试替身包括Stub、Mock和Spy。
// 生产代码
class OrderService {
constructor(paymentGateway) {
this.paymentGateway = paymentGateway;
}
processOrder(amount) {
return this.paymentGateway.charge(amount);
}
}
// 测试代码
test('OrderService should process payment', () => {
const mockGateway = {
charge: jest.fn().mockReturnValue(true)
};
const service = new OrderService(mockGateway);
service.processOrder(100);
expect(mockGateway.charge).toHaveBeenCalledWith(100);
});
这种模式下,真实支付网关被完全隔离,测试只关注业务逻辑的正确性。
工厂模式与对象创建隔离
工厂模式可以集中管理对象创建逻辑,测试时通过覆写工厂方法实现隔离:
// 用户工厂
class UserFactory {
static createAdmin() {
return new User({ role: 'admin' });
}
}
// 测试中替换工厂行为
test('Admin user should have privileges', () => {
const original = UserFactory.createAdmin;
UserFactory.createAdmin = () => new User({ role: 'admin', permissions: ['test'] });
const admin = UserFactory.createAdmin();
expect(admin.hasPermission('test')).toBeTruthy();
// 恢复原始实现
UserFactory.createAdmin = original;
});
单例模式的测试策略
单例模式需要特殊处理以避免测试间的状态污染:
class ConfigManager {
static instance;
constructor() {
if (ConfigManager.instance) {
return ConfigManager.instance;
}
this.settings = {};
ConfigManager.instance = this;
}
// 测试专用重置方法
static _resetForTesting() {
ConfigManager.instance = null;
}
}
// 在每个测试用例前后清理单例
beforeEach(() => ConfigManager._resetForTesting());
观察者模式的事件隔离
观察者模式中,需要确保事件监听不会跨测试泄露:
class EventBus {
constructor() {
this.listeners = {};
}
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
// 测试专用清理方法
_clearAllListeners() {
this.listeners = {};
}
}
// 测试用例
test('should notify listeners', () => {
const bus = new EventBus();
const mockListener = jest.fn();
bus.on('test', mockListener);
bus.emit('test');
expect(mockListener).toHaveBeenCalled();
});
afterEach(() => eventBus._clearAllListeners());
装饰器模式的测试方法
装饰器模式可以通过分层测试策略实现隔离:
function loggingDecorator(target) {
const original = target.doSomething;
target.doSomething = function(...args) {
console.log('Calling doSomething');
return original.apply(this, args);
};
return target;
}
// 测试核心功能时不应用装饰器
test('core functionality', () => {
const service = { doSomething: jest.fn() };
const result = service.doSomething('input');
expect(result).toBe(...);
});
// 单独测试装饰器行为
test('logging decorator', () => {
const mockConsole = jest.spyOn(console, 'log');
const service = { doSomething: () => {} };
loggingDecorator(service);
service.doSomething();
expect(mockConsole).toHaveBeenCalledWith('Calling doSomething');
});
策略模式的条件隔离
策略模式需要隔离不同策略的测试环境:
class ShippingCalculator {
constructor(strategy) {
this.strategy = strategy;
}
calculate(weight) {
return this.strategy(weight);
}
}
// 测试不同策略互不影响
describe('Shipping Strategies', () => {
const standardStrategy = w => w * 1.5;
const expressStrategy = w => w * 2.5;
test('standard shipping', () => {
const calculator = new ShippingCalculator(standardStrategy);
expect(calculator.calculate(10)).toBe(15);
});
test('express shipping', () => {
const calculator = new ShippingCalculator(expressStrategy);
expect(calculator.calculate(10)).toBe(25);
});
});
模块系统的隔离技巧
现代JavaScript模块系统可以通过以下方式实现隔离:
// api.js
export const fetchData = () => {
return axios.get('/api/data');
};
// 测试文件中替换模块实现
jest.mock('./api', () => ({
fetchData: jest.fn().mockResolvedValue({ data: 'mock' })
}));
test('should handle api response', async () => {
const { fetchData } = require('./api');
const result = await fetchData();
expect(result.data).toBe('mock');
});
异步操作的测试隔离
处理异步模式时需要特别注意时序隔离:
// 使用fake timers隔离setTimeout
jest.useFakeTimers();
test('debounce function', () => {
const mockFn = jest.fn();
const debounced = debounce(mockFn, 1000);
debounced();
debounced();
jest.runAllTimers();
expect(mockFn).toHaveBeenCalledTimes(1);
});
状态管理模式的隔离
对于复杂状态管理,可以采用切片测试策略:
// 测试redux reducer时隔离状态片段
test('user reducer', () => {
const initialState = { name: null };
const action = { type: 'SET_USER', payload: 'Alice' };
const newState = userReducer(initialState, action);
expect(newState.name).toBe('Alice');
});
// 单独测试action creator
test('setUser action', () => {
const action = setUser('Bob');
expect(action).toEqual({
type: 'SET_USER',
payload: 'Bob'
});
});
中间件模式的测试方法
中间件需要独立于主流程进行测试:
const loggingMiddleware = store => next => action => {
console.log('Dispatching:', action);
return next(action);
};
test('logging middleware', () => {
const mockStore = { getState: jest.fn() };
const mockNext = jest.fn();
const mockAction = { type: 'TEST' };
loggingMiddleware(mockStore)(mockNext)(mockAction);
expect(mockNext).toHaveBeenCalledWith(mockAction);
});
组合模式的层级测试
组合结构的测试需要分层验证:
class MenuComponent {
add(child) { /* ... */ }
render() { /* ... */ }
}
// 测试叶子节点
test('MenuItem render', () => {
const item = new MenuItem('Home');
expect(item.render()).toBe('<li>Home</li>');
});
// 测试组合节点
test('MenuContainer render', () => {
const menu = new MenuComponent();
menu.add(new MenuItem('About'));
expect(menu.render()).toContain('About');
});
代理模式的隔离策略
代理对象需要与真实对象隔离测试:
class Database {
query(sql) { /* 真实数据库操作 */ }
}
class DatabaseProxy {
constructor(db) {
this.db = db;
this.cache = new Map();
}
query(sql) {
if (this.cache.has(sql)) {
return this.cache.get(sql);
}
const result = this.db.query(sql);
this.cache.set(sql, result);
return result;
}
}
// 只测试代理逻辑
test('proxy caching', () => {
const mockDB = { query: jest.fn().mockReturnValue('data') };
const proxy = new DatabaseProxy(mockDB);
proxy.query('SELECT * FROM users');
proxy.query('SELECT * FROM users');
expect(mockDB.query).toHaveBeenCalledTimes(1);
});
测试数据构建模式
使用构建器模式创建测试数据可以保持测试独立性:
class UserBuilder {
constructor() {
this.user = {
id: 1,
name: 'Test User',
email: 'test@example.com'
};
}
withId(id) {
this.user.id = id;
return this;
}
build() {
return this.user;
}
}
// 在测试中按需构建
test('user with special id', () => {
const user = new UserBuilder()
.withId(999)
.build();
expect(user.id).toBe(999);
});
上一篇: 设计模式的可测试性评估