跳转到内容

JavaScript测试替身

来自代码酷

JavaScript测试替身[编辑 | 编辑源代码]

JavaScript测试替身(Test Doubles)是软件测试中用于替代真实组件的模拟对象,用于隔离被测代码并控制测试环境。在JavaScript单元测试中,测试替身能模拟依赖项(如API、数据库、外部服务)的行为,使开发者能够专注于当前模块的逻辑验证。

核心概念[编辑 | 编辑源代码]

测试替身包含以下主要类型(根据Gerard Meszaros分类):

graph LR A[Test Doubles] --> B[Dummy] A --> C[Stub] A --> D[Spy] A --> E[Mock] A --> F[Fake]

1. Dummy(占位对象)[编辑 | 编辑源代码]

  • 最简单的替身,仅用于填充参数列表,不参与实际逻辑
  • 典型场景:测试函数需要传递对象但实际不使用时
// 示例:测试需要传递但未使用的回调函数
const dummyCallback = () => {};
testFunction(dummyCallback); // dummyCallback不会被实际调用

2. Stub(桩)[编辑 | 编辑源代码]

  • 提供预定义响应,替代真实依赖的返回值
  • 特点:无状态记录,仅返回固定值
// 使用Sinon.js创建Stub
const sinon = require('sinon');
const database = {
  getUser: sinon.stub().returns({ id: 1, name: 'Test User' })
};

// 测试
console.log(database.getUser()); // 输出: { id: 1, name: 'Test User' }

3. Spy(间谍)[编辑 | 编辑源代码]

  • 包装真实函数,记录调用信息(次数、参数等)
  • 不修改原函数行为
// 使用Jest的spyOn
const user = {
  save: () => true
};

test('should call save method', () => {
  const saveSpy = jest.spyOn(user, 'save');
  user.save();
  expect(saveSpy).toHaveBeenCalledTimes(1);
});

4. Mock(模拟)[编辑 | 编辑源代码]

  • 预设行为 + 调用验证
  • 包含断言能力,验证特定交互是否发生
// 使用Jest的mock函数
const mockFetch = jest.fn()
  .mockResolvedValueOnce({ data: 'first response' })
  .mockRejectedValueOnce(new Error('Network error'));

// 测试异步场景
await mockFetch(); // 返回第一次预设值
await mockFetch(); // 抛出第二次预设错误

5. Fake(伪造对象)[编辑 | 编辑源代码]

  • 简化版真实实现,适用于测试环境
  • 典型例子:内存数据库替代真实数据库
class FakeDatabase {
  constructor() {
    this.users = [];
  }
  addUser(user) {
    this.users.push(user);
  }
}

// 测试中替代MongoDB/MySQL
const db = new FakeDatabase();
db.addUser({ id: 1 });
console.log(db.users.length); // 输出: 1

实际应用案例[编辑 | 编辑源代码]

场景1:API请求测试[编辑 | 编辑源代码]

当测试需要调用外部API的函数时:

// 原始函数
async function getUserPosts(userId) {
  const response = await fetch(`/api/users/${userId}/posts`);
  return response.json();
}

// 测试方案
test('should return user posts', async () => {
  // 1. 创建Stub替代fetch
  global.fetch = jest.fn().mockResolvedValue({
    json: () => Promise.resolve([{ id: 1, title: 'Test Post' }])
  });

  // 2. 执行测试
  const posts = await getUserPosts(123);
  
  // 3. 验证
  expect(posts).toEqual([{ id: 1, title: 'Test Post' }]);
  expect(fetch).toHaveBeenCalledWith('/api/users/123/posts');
});

场景2:定时器测试[编辑 | 编辑源代码]

测试包含`setTimeout`/`setInterval`的代码:

// 使用Jest的定时器mock
jest.useFakeTimers();

function delayedAlert(callback) {
  setTimeout(() => {
    callback('Alert!');
  }, 1000);
}

test('executes callback after delay', () => {
  const mockCallback = jest.fn();
  delayedAlert(mockCallback);
  
  // 快进时间
  jest.runAllTimers();
  
  expect(mockCallback).toHaveBeenCalledWith('Alert!');
});

数学表达[编辑 | 编辑源代码]

在测试覆盖率分析中,测试替身可帮助提高分支覆盖率。设被测函数有n个分支,使用替身后覆盖的分支数为m,则:

分支覆盖率=mn×100%

最佳实践[编辑 | 编辑源代码]

  • 适度使用:仅在测试复杂依赖时使用替身
  • 保持真实:Fake > Mock > Stub 的优先级(越接近真实实现越好)
  • 清晰命名:如`mockApiService`、`fakeLogger`等
  • 及时清理:测试后重置替身状态(如`jest.clearAllMocks()`)

常见工具[编辑 | 编辑源代码]

工具名称 类型支持 特点
Jest Mock, Spy 内置支持,无需额外库
Sinon.JS 全部类型 功能最全面的独立库
TestDouble Mock, Stub 简洁API设计
Nock HTTP Mock 专门用于网络请求模拟

通过合理使用测试替身,开发者可以构建快速、稳定且隔离的JavaScript测试环境,显著提升代码质量和开发效率。