您现在的位置是:网站首页 > 单元测试中的模式隔离技巧文章详情

单元测试中的模式隔离技巧

单元测试是保证代码质量的重要手段,而模式隔离则是确保测试独立性和可维护性的关键。在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);
});

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步